UNPKG

ava

Version:

Testing can be a drag. AVA helps you get it done.

248 lines (197 loc) 7.7 kB
'use strict'; const path = require('path'); const globby = require('globby'); const ignoreByDefault = require('ignore-by-default'); const picomatch = require('picomatch'); const slash = require('slash'); const providerManager = require('./provider-manager'); const defaultIgnorePatterns = [...ignoreByDefault.directories(), '**/node_modules']; const defaultPicomatchIgnorePatterns = [ ...defaultIgnorePatterns, // Unlike globby(), picomatch needs a complete pattern when ignoring directories. ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`) ]; const defaultMatchNoIgnore = picomatch(defaultPicomatchIgnorePatterns); const defaultIgnoredByWatcherPatterns = [ '**/*.snap.md', // No need to rerun tests when the Markdown files change. 'ava.config.js', // Config is not reloaded so avoid rerunning tests when it changes. 'ava.config.cjs' // Config is not reloaded so avoid rerunning tests when it changes. ]; const buildExtensionPattern = extensions => extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`; function normalizePattern(pattern) { // Always use `/` in patterns, harmonizing matching across platforms if (process.platform === 'win32') { pattern = slash(pattern); } if (pattern.startsWith('./')) { return pattern.slice(2); } if (pattern.startsWith('!./')) { return `!${pattern.slice(3)}`; } return pattern; } exports.normalizePattern = normalizePattern; function normalizePatterns(patterns) { return patterns.map(pattern => normalizePattern(pattern)); } exports.normalizePatterns = normalizePatterns; function normalizeGlobs({extensions, files: filePatterns, ignoredByWatcher: ignoredByWatcherPatterns, providers}) { if (filePatterns !== undefined && (!Array.isArray(filePatterns) || filePatterns.length === 0)) { throw new Error('The ’files’ configuration must be an array containing glob patterns.'); } if (ignoredByWatcherPatterns !== undefined && (!Array.isArray(ignoredByWatcherPatterns) || ignoredByWatcherPatterns.length === 0)) { throw new Error('The ’ignoredByWatcher’ configuration must be an array containing glob patterns.'); } const extensionPattern = buildExtensionPattern(extensions); const defaultTestPatterns = [ `test.${extensionPattern}`, `{src,source}/test.${extensionPattern}`, `**/__tests__/**/*.${extensionPattern}`, `**/*.spec.${extensionPattern}`, `**/*.test.${extensionPattern}`, `**/test-*.${extensionPattern}`, `**/test/**/*.${extensionPattern}`, `**/tests/**/*.${extensionPattern}`, '!**/__tests__/**/__{helper,fixture}?(s)__/**/*', '!**/test?(s)/**/{helper,fixture}?(s)/**/*' ]; if (filePatterns) { filePatterns = normalizePatterns(filePatterns); if (filePatterns.every(pattern => pattern.startsWith('!'))) { // Use defaults if patterns only contains exclusions. filePatterns = [...defaultTestPatterns, ...filePatterns]; } } else { filePatterns = defaultTestPatterns; } if (ignoredByWatcherPatterns) { ignoredByWatcherPatterns = [...defaultIgnoredByWatcherPatterns, ...normalizePatterns(ignoredByWatcherPatterns)]; } else { ignoredByWatcherPatterns = [...defaultIgnoredByWatcherPatterns]; } for (const {level, main} of providers) { if (level >= providerManager.levels.pathRewrites) { ({filePatterns, ignoredByWatcherPatterns} = main.updateGlobs({filePatterns, ignoredByWatcherPatterns})); } } return {extensions, filePatterns, ignoredByWatcherPatterns}; } exports.normalizeGlobs = normalizeGlobs; const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1)); exports.hasExtension = hasExtension; const globFiles = async (cwd, patterns) => { const files = await globby(patterns, { // Globs should work relative to the cwd value only (this should be the // project directory that AVA is run in). absolute: false, braceExpansion: true, caseSensitiveMatch: false, cwd, dot: false, expandDirectories: false, extglob: true, followSymbolicLinks: true, gitignore: false, globstar: true, ignore: defaultIgnorePatterns, baseNameMatch: false, onlyFiles: true, stats: false, unique: true }); // Return absolute file paths. This has the side-effect of normalizing paths // on Windows. return files.map(file => path.join(cwd, file)); }; async function findFiles({cwd, extensions, filePatterns}) { return (await globFiles(cwd, filePatterns)).filter(file => hasExtension(extensions, file)); } exports.findFiles = findFiles; async function findTests({cwd, extensions, filePatterns}) { return (await findFiles({cwd, extensions, filePatterns})).filter(file => !path.basename(file).startsWith('_')); } exports.findTests = findTests; function getChokidarIgnorePatterns({ignoredByWatcherPatterns}) { return [ ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`), ...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!')) ]; } exports.getChokidarIgnorePatterns = getChokidarIgnorePatterns; const matchingCache = new WeakMap(); const processMatchingPatterns = input => { let result = matchingCache.get(input); if (!result) { const ignore = [...defaultPicomatchIgnorePatterns]; const patterns = input.filter(pattern => { if (pattern.startsWith('!')) { // Unlike globby(), picomatch needs a complete pattern when ignoring directories. ignore.push(pattern.slice(1), `${pattern.slice(1)}/**/*`); return false; } return true; }); result = { match: picomatch(patterns, {ignore}), matchNoIgnore: picomatch(patterns) }; matchingCache.set(input, result); } return result; }; function matches(file, patterns) { const {match} = processMatchingPatterns(patterns); return match(file); } exports.matches = matches; const matchesIgnorePatterns = (file, patterns) => { const {matchNoIgnore} = processMatchingPatterns(patterns); return matchNoIgnore(file) || defaultMatchNoIgnore(file); }; function normalizeFileForMatching(cwd, file) { if (process.platform === 'win32') { cwd = slash(cwd); file = slash(file); } if (!cwd) { // TODO: Ensure tests provide an actual value. return file; } // TODO: If `file` is outside `cwd` we can't normalize it. Need to figure // out if that's a real-world scenario, but we may have to ensure the file // isn't even selected. if (!file.startsWith(cwd)) { return file; } // Assume `cwd` does *not* end in a slash. return file.slice(cwd.length + 1); } exports.normalizeFileForMatching = normalizeFileForMatching; function isHelperish(file) { // Assume file has been normalized already. // File names starting with an underscore are deemed "helpers". if (path.basename(file).startsWith('_')) { return true; } // This function assumes the file has been normalized. If it couldn't be, // don't check if it's got a parent directory that starts with an underscore. // Deem it not a "helper". if (path.isAbsolute(file)) { return false; } // If the file has a parent directory that starts with only a single // underscore, it's deemed a "helper". return path.dirname(file).split('/').some(dir => /^_(?:$|[^_])/.test(dir)); } exports.isHelperish = isHelperish; function classify(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) { file = normalizeFileForMatching(cwd, file); return { isIgnoredByWatcher: matchesIgnorePatterns(file, ignoredByWatcherPatterns), isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns) }; } exports.classify = classify; function applyTestFileFilter({cwd, filter, testFiles}) { return testFiles.filter(file => matches(normalizeFileForMatching(cwd, file), filter)); } exports.applyTestFileFilter = applyTestFileFilter;