UNPKG

@stryker-mutator/karma-runner

Version:

A plugin to use the karma test runner in Stryker, the JavaScript mutation testing framework

242 lines (215 loc) 7.58 kB
import path from 'path'; import { fileURLToPath } from 'url'; import { Logger, LoggerFactoryMethod } from '@stryker-mutator/api/logging'; import type { Config, ConfigOptions, ClientOptions, InlinePluginType, } from 'karma'; import { noopLogger, requireResolve } from '@stryker-mutator/util'; import { StrykerReporter, strykerReporterFactory } from './stryker-reporter.js'; import { TestHooksMiddleware, TEST_HOOKS_FILE_NAME, } from './test-hooks-middleware.js'; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); export const strykerKarmaConfigPath = path.resolve( path.dirname(filename), '..' /* karma-plugins */, '..' /* src */, '..' /* dist */, 'stryker-karma.conf.cjs', ); function setDefaultOptions(config: Config) { config.set({ browsers: ['ChromeHeadless'], frameworks: ['jasmine'], }); } async function setUserKarmaConfigFile( config: Config, log: Logger, requireFromCwd: typeof requireResolve, ) { if (globalSettings.karmaConfigFile) { const configFileName = path.resolve(globalSettings.karmaConfigFile); log.debug('Importing config from "%s"', configFileName); try { const userConfig = requireFromCwd(configFileName); if (typeof userConfig !== 'function') { throw new TypeError( `Karma config file "${configFileName}" should export a function! Found: ${typeof userConfig}`, ); } await userConfig(config); config.configFile = configFileName; // override config to ensure karma is as user-like as possible } catch (error: any) { if (error.code === 'MODULE_NOT_FOUND') { log.error( `Unable to find karma config at "${globalSettings.karmaConfigFile}" (tried to load from ${configFileName}). Please check your stryker config. You might need to make sure the file is included in the sandbox directory.`, ); } else { throw error; // oops } } } } /** * Sets configuration that is needed to control the karma life cycle. Namely it shouldn't watch files and not quit after first test run. * @param config The config to use */ function setLifeCycleOptions(config: Config) { config.set({ // No auto watch, stryker will inform us when we need to test autoWatch: false, // Override browserNoActivityTimeout. Default value 10000 might not enough to send perTest coverage results browserNoActivityTimeout: 1000000, // Never detach, always run in this same process (is already a separate process) detached: false, // Don't stop after first run singleRun: false, }); } /** * Sets configuration that is needed to control client scripts in karma. * @param config The config to use * @see https://github.com/stryker-mutator/stryker-js/issues/2049 */ function setClientOptions(config: Config) { // Disable clearContext because of issue #2049 (race condition in Karma) // Enabling clearContext (default true) will load "about:blank" in the iFrame after a test run. // As far as I can see clearing the context only has a visible effect (you don't see the result of the last test). // If this is true, disabling it is safe to do and solves the race condition issue. const clientOptions: Partial<ClientOptions> = { clearContext: false }; // Disable randomized tests with using jasmine. Stryker doesn't play nice with a random test order, since spec id's tent to move around // Also set failFast, so that we're not waiting on more than 1 failed test if (config.frameworks?.includes('jasmine')) { (clientOptions as any).jasmine = { random: false, // Jasmine@<4 failFast: !globalSettings.disableBail, oneFailurePerSpec: !globalSettings.disableBail, // Jasmine@4 stopOnSpecFailure: !globalSettings.disableBail, stopSpecOnExpectationFailure: !globalSettings.disableBail, }; } if (config.frameworks?.includes('mocha')) { (clientOptions as any).mocha = { bail: !globalSettings.disableBail }; } config.set({ client: clientOptions }); } function setUserKarmaConfig(config: Config) { if (globalSettings.karmaConfig) { config.set(globalSettings.karmaConfig); } } function setBasePath(config: Config) { if (!config.basePath) { // We need to set the base path, so karma won't use this file to base everything of if (globalSettings.karmaConfigFile) { config.basePath = path.resolve( path.dirname(globalSettings.karmaConfigFile), ); } else { config.basePath = process.cwd(); } } } function addPlugin( karmaConfig: ConfigOptions, karmaPlugin: Record<string, InlinePluginType> | string, ) { karmaConfig.plugins = karmaConfig.plugins ?? ['karma-*']; karmaConfig.plugins.push( typeof karmaPlugin === 'string' ? import.meta.resolve(karmaPlugin) : karmaPlugin, ); } /** * Configures the test hooks middleware. * It adds a non-existing file to the top `files` array. * Further more it configures a middleware that serves the file. */ function configureTestHooksMiddleware(config: Config) { // Add test run middleware file config.files = config.files ?? []; config.files.unshift({ pattern: TEST_HOOKS_FILE_NAME, included: true, watched: false, served: false, nocache: true, }); // Add a custom hooks file to provide hooks const middleware: string[] = config.beforeMiddleware ?? (config.beforeMiddleware = []); middleware.unshift(TestHooksMiddleware.name); TestHooksMiddleware.instance.configureTestFramework(config.frameworks); addPlugin(config, { [`middleware:${TestHooksMiddleware.name}`]: [ 'value', TestHooksMiddleware.instance.handler, ], }); } function configureStrykerMutantCoverageAdapter(config: Config) { config.files = config.files ?? []; config.files.unshift({ pattern: path.resolve(dirname, 'stryker-mutant-coverage-adapter.js'), included: true, watched: false, served: true, nocache: true, type: 'module', }); } function configureStrykerReporter(config: Config) { addPlugin(config, { [`reporter:${StrykerReporter.name}`]: ['factory', strykerReporterFactory], }); if (!config.reporters) { config.reporters = []; } config.reporters.push(StrykerReporter.name); } interface GlobalSettings { karmaConfig?: ConfigOptions; karmaConfigFile?: string; getLogger: LoggerFactoryMethod; disableBail: boolean; } const globalSettings: GlobalSettings = { getLogger() { return noopLogger; }, disableBail: false, }; export async function configureKarma( config: Config, requireFromCwd = requireResolve, ): Promise<void> { const log = globalSettings.getLogger(path.basename(filename)); setDefaultOptions(config); await setUserKarmaConfigFile(config, log, requireFromCwd); setUserKarmaConfig(config); setBasePath(config); setLifeCycleOptions(config); setClientOptions(config); configureTestHooksMiddleware(config); configureStrykerMutantCoverageAdapter(config); configureStrykerReporter(config); } /** * Provide global settings for next configuration * This is the only way we can pass through any values between the `KarmaTestRunner` and the stryker-karma.conf file. * (not counting environment variables) */ configureKarma.setGlobals = (globals: Partial<GlobalSettings>) => { globalSettings.karmaConfig = globals.karmaConfig; globalSettings.karmaConfigFile = globals.karmaConfigFile; globalSettings.getLogger = globals.getLogger ?? (() => noopLogger); globalSettings.disableBail = globals.disableBail ?? false; };