@eeacms/volto-chatbot
Version:
@eeacms/volto-chatbot: Volto add-on
478 lines (424 loc) • 13.8 kB
JavaScript
/**
* Generic Jest configuration for Volto addons
*
* This configuration automatically:
* - Detects the addon name from the config file path
* - Configures test coverage to focus on the specific test path
* - Handles different ways of specifying test paths:
* - Full paths like src/addons/addon-name/src/components
* - Just filenames like Component.test.jsx
* - Just directory names like components
*
* Usage:
* RAZZLE_JEST_CONFIG=src/addons/addon-name/jest-addon.config.js CI=true yarn test [test-path] --collectCoverage
*/
require('dotenv').config({ path: __dirname + '/.env' });
const path = require('path');
const fs = require('fs');
const fg = require('fast-glob');
// Get the addon name from the current file path
const pathParts = __filename.split(path.sep);
const addonsIdx = pathParts.lastIndexOf('addons');
const addonName =
addonsIdx !== -1 && addonsIdx < pathParts.length - 1
? pathParts[addonsIdx + 1]
: path.basename(path.dirname(__filename)); // Fallback to folder name
const addonBasePath = `src/addons/${addonName}/src`;
const voltoSlatePath = fs.existsSync(
path.join(
__dirname,
'..',
'..',
'..',
'node_modules',
'@plone',
'volto-slate',
'src',
),
)
? '<rootDir>/node_modules/@plone/volto-slate/src'
: '<rootDir>/node_modules/@plone/volto/packages/volto-slate/src';
// --- Performance caches ---
const fileSearchCache = new Map();
const dirSearchCache = new Map();
const dirListingCache = new Map();
const statCache = new Map();
const implementationCache = new Map();
/**
* Cached fs.statSync wrapper to avoid redundant filesystem calls
* @param {string} p
* @returns {fs.Stats|null}
*/
const getStatSync = (p) => {
if (statCache.has(p)) return statCache.get(p);
try {
const s = fs.statSync(p);
statCache.set(p, s);
return s;
} catch {
statCache.set(p, null);
return null;
}
};
/**
* Find files that match a specific pattern using fast-glob
* @param {string} baseDir - The base directory to search in
* @param {string} fileName - The name of the file to find
* @param {string} [pathPattern=''] - Optional path pattern to filter results
* @returns {string[]} - Array of matching file paths
*/
const findFilesWithPattern = (baseDir, fileName, pathPattern = '') => {
const cacheKey = `${baseDir}|${fileName}|${pathPattern}`;
if (fileSearchCache.has(cacheKey)) {
return fileSearchCache.get(cacheKey);
}
let files = [];
try {
const patterns = fileName
? [`${baseDir}/**/${fileName}`]
: [`${baseDir}/**/*.{js,jsx,ts,tsx}`];
files = fg.sync(patterns, { onlyFiles: true });
if (pathPattern) {
files = files.filter((file) => file.includes(pathPattern));
}
} catch {
files = [];
}
fileSearchCache.set(cacheKey, files);
return files;
};
/**
* Find directories that match a specific pattern using fast-glob
* @param {string} baseDir - The base directory to search in
* @param {string} dirName - The name of the directory to find
* @param {string} [pathPattern=''] - Optional path pattern to filter results
* @returns {string[]} - Array of matching directory paths
*/
const findDirsWithPattern = (baseDir, dirName, pathPattern = '') => {
const cacheKey = `${baseDir}|${dirName}|${pathPattern}`;
if (dirSearchCache.has(cacheKey)) {
return dirSearchCache.get(cacheKey);
}
let dirs = [];
try {
const patterns = dirName
? [`${baseDir}/**/${dirName}`]
: [`${baseDir}/**/`];
dirs = fg.sync(patterns, { onlyDirectories: true });
if (pathPattern) {
dirs = dirs.filter((dir) => dir.includes(pathPattern));
}
} catch {
dirs = [];
}
dirSearchCache.set(cacheKey, dirs);
return dirs;
};
/**
* Find files or directories in the addon using fast-glob
* @param {string} name - The name to search for
* @param {string} type - The type of item to find ('f' for files, 'd' for directories)
* @param {string} [additionalOptions=''] - Additional options for flexible path matching
* @returns {string|null} - The path of the found item or null if not found
*/
const findInAddon = (name, type, additionalOptions = '') => {
const isFile = type === 'f';
const isDirectory = type === 'd';
const isFlexiblePathMatch = additionalOptions.includes('-path');
let pathPattern = '';
if (isFlexiblePathMatch) {
const match = additionalOptions.match(/-path "([^"]+)"/);
if (match && match[1]) {
pathPattern = match[1].replace(/\*/g, '');
}
}
try {
let results = [];
if (isFile) {
results = findFilesWithPattern(addonBasePath, name, pathPattern);
} else if (isDirectory) {
results = findDirsWithPattern(addonBasePath, name, pathPattern);
}
return results.length > 0 ? results[0] : null;
} catch (error) {
return null;
}
};
/**
* Find the implementation file for a test file
* @param {string} testPath - Path to the test file
* @returns {string|null} - Path to the implementation file or null if not found
*/
const findImplementationFile = (testPath) => {
if (implementationCache.has(testPath)) {
return implementationCache.get(testPath);
}
if (!fs.existsSync(testPath)) {
implementationCache.set(testPath, null);
return null;
}
const dirPath = path.dirname(testPath);
const fileName = path.basename(testPath);
// Regex for common test file patterns (e.g., .test.js, .spec.ts)
const TEST_OR_SPEC_FILE_REGEX = /\.(test|spec)\.[jt]sx?$/;
if (!TEST_OR_SPEC_FILE_REGEX.test(fileName)) {
implementationCache.set(testPath, null);
return null;
}
const baseFileName = path
.basename(fileName, path.extname(fileName))
.replace(/\.(test|spec)$/, ''); // Remove .test or .spec
let dirFiles = dirListingCache.get(dirPath);
if (!dirFiles) {
dirFiles = fs.readdirSync(dirPath);
dirListingCache.set(dirPath, dirFiles);
}
const exactMatch = dirFiles.find((file) => {
const fileBaseName = path.basename(file, path.extname(file));
return (
fileBaseName === baseFileName && !TEST_OR_SPEC_FILE_REGEX.test(file) // Ensure it's not another test/spec file
);
});
if (exactMatch) {
const result = `${dirPath}/${exactMatch}`;
implementationCache.set(testPath, result);
return result;
}
const similarMatch = dirFiles.find((file) => {
if (
TEST_OR_SPEC_FILE_REGEX.test(file) ||
(getStatSync(`${dirPath}/${file}`)?.isDirectory() ?? false)
) {
return false;
}
const fileBaseName = path.basename(file, path.extname(file));
return (
fileBaseName.toLowerCase().includes(baseFileName.toLowerCase()) ||
baseFileName.toLowerCase().includes(fileBaseName.toLowerCase())
);
});
if (similarMatch) {
const result = `${dirPath}/${similarMatch}`;
implementationCache.set(testPath, result);
return result;
}
implementationCache.set(testPath, null);
return null;
};
/**
* Get the test path from command line arguments
* @returns {string|null} - The resolved test path or null if not found
*/
const getTestPath = () => {
const args = process.argv;
let testPath = null;
const TEST_FILE_REGEX = /\.test\.[jt]sx?$/; // Matches .test.js, .test.jsx, .test.ts, .test.tsx
testPath = args.find(
(arg) =>
arg.includes(addonName) &&
!arg.startsWith('--') &&
arg !== 'test' &&
arg !== 'node',
);
if (!testPath) {
const testIndex = args.findIndex((arg) => arg === 'test');
if (testIndex !== -1 && testIndex < args.length - 1) {
const nextArg = args[testIndex + 1];
if (!nextArg.startsWith('--')) {
testPath = nextArg;
}
}
}
if (!testPath) {
testPath = args.find((arg) => TEST_FILE_REGEX.test(arg));
}
if (!testPath) {
return null;
}
if (!testPath.includes(path.sep)) {
if (TEST_FILE_REGEX.test(testPath)) {
const foundTestFile = findInAddon(testPath, 'f');
if (foundTestFile) {
return foundTestFile;
}
} else {
const foundDir = findInAddon(testPath, 'd');
if (foundDir) {
return foundDir;
}
const flexibleDir = findInAddon(testPath, 'd', `-path "*${testPath}*"`);
if (flexibleDir) {
return flexibleDir;
}
}
} else if (
TEST_FILE_REGEX.test(testPath) && // Check if it looks like a test file path
!testPath.startsWith('src/addons/')
) {
const testFileName = path.basename(testPath);
const foundTestFile = findInAddon(testFileName, 'f');
if (foundTestFile) {
const relativePath = path.dirname(testPath);
if (foundTestFile.includes(relativePath)) {
return foundTestFile;
}
const similarFiles = findFilesWithPattern(
addonBasePath,
testFileName,
relativePath,
);
if (similarFiles && similarFiles.length > 0) {
return similarFiles[0];
}
}
}
if (
!path
.normalize(testPath)
.startsWith(path.join('src', 'addons', addonName, 'src')) &&
!path.isAbsolute(testPath) // Use path.isAbsolute for robust check
) {
testPath = path.join(addonBasePath, testPath); // Use path.join for OS-agnostic paths
}
if (fs.existsSync(testPath)) {
return testPath;
}
const pathWithoutTrailingSlash = testPath.endsWith(path.sep)
? testPath.slice(0, -1)
: null;
if (pathWithoutTrailingSlash && fs.existsSync(pathWithoutTrailingSlash)) {
return pathWithoutTrailingSlash;
}
const pathWithTrailingSlash = !testPath.endsWith(path.sep)
? testPath + path.sep
: null;
if (pathWithTrailingSlash && fs.existsSync(pathWithTrailingSlash)) {
// Generally, return paths without trailing slashes for consistency,
// unless it's specifically needed for a directory that only exists with it (rare).
return testPath;
}
return testPath; // Return the original path if no variations exist
};
/**
* Determine collectCoverageFrom patterns based on test path
* @returns {string[]} - Array of coverage patterns
*/
const getCoveragePatterns = () => {
const excludePatterns = [
'!src/**/*.d.ts',
'!**/*.test.{js,jsx,ts,tsx}',
'!**/*.spec.{js,jsx,ts,tsx}',
];
const defaultPatterns = [
`${addonBasePath}/**/*.{js,jsx,ts,tsx}`,
...excludePatterns,
];
const ANY_SCRIPT_FILE_REGEX = /\.[jt]sx?$/;
const directoryArg = process.argv.find(
(arg) =>
!arg.includes(path.sep) &&
!arg.startsWith('--') &&
arg !== 'test' &&
arg !== 'node' &&
!ANY_SCRIPT_FILE_REGEX.test(arg) &&
![
'yarn',
'npm',
'npx',
'collectCoverage',
'CI',
'RAZZLE_JEST_CONFIG',
].some(
(reserved) =>
arg === reserved || arg.startsWith(reserved.split('=')[0] + '='),
) &&
process.argv.indexOf(arg) >
process.argv.findIndex((item) => item === 'test'),
);
if (directoryArg) {
const foundDir = findInAddon(directoryArg, 'd');
if (foundDir) {
return [`${foundDir}/**/*.{js,jsx,ts,tsx}`, ...excludePatterns];
}
}
let testPath = getTestPath();
if (!testPath) {
return defaultPatterns;
}
if (testPath.endsWith(path.sep)) {
testPath = testPath.slice(0, -1);
}
const stats = getStatSync(testPath);
if (stats && stats.isFile()) {
const implFile = findImplementationFile(testPath);
if (implFile) {
return [implFile, '!src/**/*.d.ts'];
}
const dirPath = path.dirname(testPath);
return [`${dirPath}/**/*.{js,jsx,ts,tsx}`, ...excludePatterns];
} else if (stats && stats.isDirectory()) {
return [`${testPath}/**/*.{js,jsx,ts,tsx}`, ...excludePatterns];
}
return defaultPatterns;
};
const coverageConfig = getCoveragePatterns();
module.exports = {
testMatch: ['**/src/addons/**/?(*.)+(spec|test).[jt]s?(x)'],
collectCoverageFrom: coverageConfig,
coveragePathIgnorePatterns: [
'/node_modules/',
'schema\\.[jt]s?$',
'index\\.[jt]s?$',
'config\\.[jt]sx?$',
],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'@plone/volto/cypress': '<rootDir>/node_modules/@plone/volto/cypress',
'@plone/volto/babel': '<rootDir>/node_modules/@plone/volto/babel',
'@plone/volto/(.*)$': '<rootDir>/node_modules/@plone/volto/src/$1',
'@package/(.*)$': '<rootDir>/node_modules/@plone/volto/src/$1',
'@root/(.*)$': '<rootDir>/node_modules/@plone/volto/src/$1',
'@plone/volto-quanta/(.*)$': '<rootDir>/src/addons/volto-quanta/src/$1',
'@eeacms/search/(.*)$': '<rootDir>/src/addons/volto-searchlib/searchlib/$1',
'@eeacms/search': '<rootDir>/src/addons/volto-searchlib/searchlib',
'@eeacms/(.*?)/(.*)$': '<rootDir>/node_modules/@eeacms/$1/src/$2',
'@plone/volto-slate$': voltoSlatePath,
'@plone/volto-slate/(.*)$': `${voltoSlatePath}/$1`,
'~/(.*)$': '<rootDir>/src/$1',
'load-volto-addons':
'<rootDir>/node_modules/@plone/volto/jest-addons-loader.js',
},
transformIgnorePatterns: [
'/node_modules/(?!(@plone|@root|@package|@eeacms)/).*/',
],
transform: {
'^.+\\.js(x)?$': 'babel-jest',
'^.+\\.ts(x)?$': 'babel-jest',
'^.+\\.(png)$': 'jest-file',
'^.+\\.(jpg)$': 'jest-file',
'^.+\\.(svg)$': './node_modules/@plone/volto/jest-svgsystem-transform.js',
},
coverageThreshold: {
global: {
branches: 5,
functions: 5,
lines: 5,
statements: 5,
},
},
...(process.env.JEST_USE_SETUP === 'ON' && {
setupFilesAfterEnv: [
fs.existsSync(
path.join(
__dirname,
'node_modules',
'@eeacms',
addonName,
'jest.setup.js',
),
)
? `<rootDir>/node_modules/@eeacms/${addonName}/jest.setup.js`
: `<rootDir>/src/addons/${addonName}/jest.setup.js`,
],
}),
};