refactor(*): break up into individual modules (#474)

this should make it much easier to comprehend
and write tests for karma-webpack.
there is one change in how the
KarmaWebpackController is managed, we now
instantiate this in the preprocessor phase
and propagate the value within the karma config
object as a private variable. This allows
for breaking the framework and preprocessor
into separates modules and has the added
benefit of being able to run multiple
times in a given session without sharing
mutable state. This allows integrations
tests to be run in parallel as well as multiple
times which was previously not possible.

Fixes N/A
This commit is contained in:
Cody Mikol 2021-01-31 20:24:42 -05:00 committed by GitHub
parent 8ad09d1551
commit 53002008f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 265 additions and 201 deletions

View file

@ -1,9 +1,13 @@
module.exports = {
root: true,
globals: {
"jasmine": true,
},
plugins: ['prettier'],
extends: ['@webpack-contrib/eslint-config-webpack'],
rules: {
"consistent-return": "off",
"camelcase": "off",
"no-console": "off",
"no-param-reassign": "off",
"no-underscore-dangle": "off",

View file

@ -1 +1 @@
module.exports = require('./karma-webpack');
module.exports = require('./karma/plugin');

View file

@ -1,75 +1,17 @@
const path = require('path');
const fs = require('fs');
const os = require('os');
const webpack = require('webpack');
const merge = require('webpack-merge');
class KarmaSyncPlugin {
constructor(options) {
this.karmaEmitter = options.karmaEmitter;
this.controller = options.controller;
const KW_WebpackPlugin = require('../webpack/plugin');
const DefaultWebpackOptionsFactory = require('../webpack/defaults');
class KW_Controller {
constructor() {
this.isActive = false;
this.bundlesContent = {};
this.hasBeenBuiltAtLeastOnce = false;
this.webpackOptions = DefaultWebpackOptionsFactory.create();
}
apply(compiler) {
this.compiler = compiler;
// webpack bundles are finished
compiler.hooks.done.tap('KarmaSyncPlugin', async (stats) => {
// read generated file content and store for karma preprocessor
this.controller.bundlesContent = {};
stats.toJson().assets.forEach((webpackFileObj) => {
const filePath = `${compiler.options.output.path}/${webpackFileObj.name}`;
this.controller.bundlesContent[webpackFileObj.name] = fs.readFileSync(
filePath,
'utf-8'
);
});
// karma refresh
this.karmaEmitter.refreshFiles();
});
}
}
const defaultWebpackOptions = {
mode: 'development',
output: {
filename: '[name].js',
// eslint-disable-next-line prettier/prettier
path: path.join(os.tmpdir(), '_karma_webpack_') + Math.floor(Math.random() * 1000000),
},
stats: {
modules: false,
colors: true,
},
watch: false,
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 1,
},
},
},
},
plugins: [],
// Something like this will be auto added by this.configure()
// entry: {
// 'foo-one.test.js': 'path/to/test/foo-one.test.js',
// 'foo-two.test.js': 'path/to/test/foo-two.test.js',
// },
// plugins: [
// new KarmaSyncPlugin()
// ],
};
class KarmaWebpackController {
set webpackOptions(options) {
this.__webpackOptions = options;
}
@ -78,11 +20,15 @@ class KarmaWebpackController {
return this.__webpackOptions;
}
updateWebpackOptions(newOptions) {
this.webpackOptions = merge(this.webpackOptions, newOptions);
}
set karmaEmitter(emitter) {
this.__karmaEmitter = emitter;
this.__webpackOptions.plugins.push(
new KarmaSyncPlugin({
new KW_WebpackPlugin({
karmaEmitter: emitter,
controller: this,
})
@ -97,13 +43,6 @@ class KarmaWebpackController {
return this.webpackOptions.output.path;
}
constructor() {
this.isActive = false;
this.bundlesContent = {};
this.hasBeenBuiltAtLeastOnce = false;
this.webpackOptions = defaultWebpackOptions;
}
setupExitHandler(compiler) {
this.karmaEmitter.once('exit', (done) => {
compiler.close(() => {
@ -113,10 +52,6 @@ class KarmaWebpackController {
});
}
updateWebpackOptions(newOptions) {
this.webpackOptions = merge(this.webpackOptions, newOptions);
}
async bundle() {
if (this.isActive === false && this.hasBeenBuiltAtLeastOnce === false) {
console.log('Webpack bundling...');
@ -169,8 +104,4 @@ class KarmaWebpackController {
}
}
module.exports = {
KarmaSyncPlugin,
KarmaWebpackController,
defaultWebpackOptions,
};
module.exports = KW_Controller;

View file

@ -0,0 +1,36 @@
const fs = require('fs');
const path = require('path');
function KW_Framework(config) {
// This controller is instantiated and set during the preprocessor phase.
const controller = config.__karmaWebpackController;
const commonsPath = path.join(controller.outputPath, 'commons.js');
const runtimePath = path.join(controller.outputPath, 'runtime.js');
// make sure tmp folder exists
if (!fs.existsSync(controller.outputPath)) {
fs.mkdirSync(controller.outputPath);
}
// create dummy files for commons.js and runtime.js so they get included by karma
fs.closeSync(fs.openSync(commonsPath, 'w'));
fs.closeSync(fs.openSync(runtimePath, 'w'));
// register for karma
config.files.unshift({
pattern: commonsPath,
included: true,
served: true,
watched: false,
});
config.files.unshift({
pattern: runtimePath,
included: true,
served: true,
watched: false,
});
}
KW_Framework.$inject = ['config'];
module.exports = KW_Framework;

View file

@ -1,45 +1,12 @@
const path = require('path');
const fs = require('fs');
const glob = require('glob');
const minimatch = require('minimatch');
const { ensureWebpackFrameworkSet } = require('./karma/karmaConfigValidator');
const { ensureWebpackFrameworkSet } = require('../karma/validation');
const { hash } = require('../utils/hash');
const { hash } = require('./utils/hash');
const { KarmaWebpackController } = require('./KarmaWebpackController');
const controller = new KarmaWebpackController();
function registerExtraWebpackFiles(config, _controller) {
const localController = _controller || controller;
const commonsPath = path.join(localController.outputPath, 'commons.js');
const runtimePath = path.join(localController.outputPath, 'runtime.js');
// make sure tmp folder exists
if (!fs.existsSync(localController.outputPath)) {
fs.mkdirSync(localController.outputPath);
}
// create dummy files for commons.js and runtime.js so they get included by karma
fs.closeSync(fs.openSync(commonsPath, 'w'));
fs.closeSync(fs.openSync(runtimePath, 'w'));
// register for karma
config.files.unshift({
pattern: commonsPath,
included: true,
served: true,
watched: false,
});
config.files.unshift({
pattern: runtimePath,
included: true,
served: true,
watched: false,
});
}
const KW_Controller = require('./controller');
function getPathKey(filePath, withExtension = false) {
const pathParts = path.parse(filePath);
@ -81,7 +48,9 @@ function configToWebpackEntries(config) {
return webpackEntries;
}
function preprocessorFactory(config, emitter) {
function KW_Preprocessor(config, emitter) {
const controller = new KW_Controller();
config.__karmaWebpackController = controller;
ensureWebpackFrameworkSet(config);
// one time setup
@ -118,12 +87,6 @@ function preprocessorFactory(config, emitter) {
};
}
registerExtraWebpackFiles.$inject = ['config'];
preprocessorFactory.$inject = ['config', 'emitter'];
KW_Preprocessor.$inject = ['config', 'emitter'];
module.exports = {
'preprocessor:webpack': ['factory', preprocessorFactory],
'framework:webpack': ['factory', registerExtraWebpackFiles],
registerExtraWebpackFiles,
configToWebpackEntries,
};
module.exports = KW_Preprocessor;

9
lib/karma/plugin.js Normal file
View file

@ -0,0 +1,9 @@
const KW_Framework = require('../karma-webpack/framework');
const KW_Preprocessor = require('../karma-webpack/preprocessor');
const KW_KarmaPlugin = {
'preprocessor:webpack': ['factory', KW_Preprocessor],
'framework:webpack': ['factory', KW_Framework],
};
module.exports = KW_KarmaPlugin;

35
lib/webpack/defaults.js Normal file
View file

@ -0,0 +1,35 @@
const path = require('path');
const os = require('os');
function create() {
return {
mode: 'development',
output: {
filename: '[name].js',
// eslint-disable-next-line prettier/prettier
path: path.join(os.tmpdir(), '_karma_webpack_') + Math.floor(Math.random() * 1000000),
},
stats: {
modules: false,
colors: true,
},
watch: false,
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 1,
},
},
},
},
plugins: [],
};
}
module.exports = { create };

32
lib/webpack/plugin.js Normal file
View file

@ -0,0 +1,32 @@
const fs = require('fs');
class KW_WebpackPlugin {
constructor(options) {
this.karmaEmitter = options.karmaEmitter;
this.controller = options.controller;
}
apply(compiler) {
this.compiler = compiler;
// webpack bundles are finished
compiler.hooks.done.tap('KW_WebpackPlugin', async (stats) => {
// read generated file content and store for karma preprocessor
this.controller.bundlesContent = {};
stats.toJson().assets.forEach((webpackFileObj) => {
const filePath = `${compiler.options.output.path}/${
webpackFileObj.name
}`;
this.controller.bundlesContent[webpackFileObj.name] = fs.readFileSync(
filePath,
'utf-8'
);
});
// karma refresh
this.karmaEmitter.refreshFiles();
});
}
}
module.exports = KW_WebpackPlugin;

View file

@ -4,7 +4,7 @@ import karmaChromeLauncher from 'karma-chrome-launcher';
import karmaMocha from 'karma-mocha';
import karmaChai from 'karma-chai';
import ScenarioUtils from '../../utils/ScenarioUtils';
import Scenario from '../../utils/scenario';
process.env.CHROME_BIN = require('puppeteer').executablePath();
@ -36,7 +36,7 @@ describe('A basic karma-webpack setup', () => {
};
beforeAll((done) => {
ScenarioUtils.run(config)
Scenario.run(config)
.then((res) => {
scenario = res;
})

View file

@ -1,6 +1,6 @@
const karma = require('karma');
const ScenarioUtils = { run };
const Scenario = { run };
/**
* This allows you to run karma with a given configuration and be returned.
@ -20,4 +20,4 @@ function run(config) {
});
}
export default ScenarioUtils;
export default Scenario;

View file

@ -1,26 +0,0 @@
const {
KarmaWebpackController,
defaultWebpackOptions,
} = require('../../lib/KarmaWebpackController');
describe('KarmaWebpackController', () => {
it('applies the default webpackOptions', () => {
const controller = new KarmaWebpackController();
expect(controller.webpackOptions).toEqual(defaultWebpackOptions);
});
it('can provide custom nested webpackOptions', () => {
const controller = new KarmaWebpackController();
controller.updateWebpackOptions({
output: {
path: 'foo',
publicPath: 'bar',
},
});
expect(controller.webpackOptions.output.path).toBe('foo');
expect(controller.webpackOptions.output.publicPath).toBe('bar');
expect(controller.webpackOptions.output.filename).toBe(
defaultWebpackOptions.output.filename
);
});
});

View file

@ -1,38 +0,0 @@
const fs = require('fs');
const path = require('path');
const { registerExtraWebpackFiles } = require('../../lib/karma-webpack');
jest.mock('fs');
describe('karma-webpack', () => {
describe('registerExtraWebpackFiles()', () => {
test('Defaults', () => {
const controller = { outputPath: 'foo/' };
const config = { files: [] };
fs.closeSync = jest.fn();
fs.openSync = jest.fn();
registerExtraWebpackFiles(config, controller);
expect(fs.openSync).toBeCalledWith(path.join('foo', 'commons.js'), 'w');
expect(fs.openSync).toBeCalledWith(path.join('foo', 'runtime.js'), 'w');
expect(config.files.length).toBe(2);
expect(config.files).toEqual([
{
pattern: path.join('foo', 'runtime.js'),
included: true,
served: true,
watched: false,
},
{
pattern: path.join('foo', 'commons.js'),
included: true,
served: true,
watched: false,
},
]);
});
});
});

View file

@ -0,0 +1,52 @@
const KW_Controller = require('../../../lib/karma-webpack/controller');
const DefaultWebpackOptionsFactory = require('../../../lib/webpack/defaults');
const defaultWebpackOptions = DefaultWebpackOptionsFactory.create();
describe('KW_Controller', () => {
const EXPECTED_DEFAULT_PATH_PREFIX = '/tmp/_karma_webpack_';
let controller;
beforeEach(() => (controller = new KW_Controller()));
it('initializes with a webpackOptions object', () => {
expect(controller.webpackOptions).toBeDefined();
expect(controller.webpackOptions).toEqual(jasmine.any(Object));
});
it('correctly sets the default output path prefix', () => {
expect(
controller.webpackOptions.output.path.startsWith(
EXPECTED_DEFAULT_PATH_PREFIX
)
).toBeTruthy();
});
it('correctly postfixes a random number to the end of the webpack options output path for parallel runs', () => {
const postfix = controller.webpackOptions.output.path.split(
EXPECTED_DEFAULT_PATH_PREFIX
)[1];
expect(isNaN(postfix)).toBe(false);
});
it('should otherwise be equal to a newly instantiated default webpack options object', () => {
controller.webpackOptions.output.path = EXPECTED_DEFAULT_PATH_PREFIX;
defaultWebpackOptions.output.path = EXPECTED_DEFAULT_PATH_PREFIX;
expect(controller.webpackOptions).toEqual(defaultWebpackOptions);
});
it('can provide custom nested webpackOptions', () => {
controller.updateWebpackOptions({
output: {
path: 'foo',
publicPath: 'bar',
},
});
expect(controller.webpackOptions.output.path).toBe('foo');
expect(controller.webpackOptions.output.publicPath).toBe('bar');
expect(controller.webpackOptions.output.filename).toBe(
defaultWebpackOptions.output.filename
);
});
});

View file

@ -0,0 +1,36 @@
const fs = require('fs');
const path = require('path');
const KW_Framework = require('../../../lib/karma-webpack/framework');
jest.mock('fs');
describe('KW_Framework', () => {
test('Defaults', () => {
const controller = { outputPath: 'foo/' };
const config = { files: [], __karmaWebpackController: controller };
fs.closeSync = jest.fn();
fs.openSync = jest.fn();
KW_Framework(config);
expect(fs.openSync).toBeCalledWith(path.join('foo', 'commons.js'), 'w');
expect(fs.openSync).toBeCalledWith(path.join('foo', 'runtime.js'), 'w');
expect(config.files.length).toBe(2);
expect(config.files).toEqual([
{
pattern: path.join('foo', 'runtime.js'),
included: true,
served: true,
watched: false,
},
{
pattern: path.join('foo', 'commons.js'),
included: true,
served: true,
watched: false,
},
]);
});
});

View file

@ -0,0 +1,8 @@
const KW_Preprocessor = require('../../../lib/karma-webpack/preprocessor');
describe('KW_Preprocessor', () => {
it('should be defined', () => {
expect(KW_Preprocessor).toBeDefined();
});
// todo(mikol): write a KW_Preprocessor test suite before v5 official release.
});

View file

@ -0,0 +1,8 @@
const KW_KarmaPlugin = require('../../../lib/karma/plugin');
describe('KW_KarmaPlugin', () => {
it('should be defined', () => {
expect(KW_KarmaPlugin).toBeDefined();
});
// todo(mikol): write test suite for KarmaWebpackPlugin prior to v5 official release.
});

View file

@ -1,6 +1,4 @@
const {
ensureWebpackFrameworkSet,
} = require('../../../lib/karma/karmaConfigValidator');
const { ensureWebpackFrameworkSet } = require('../../../lib/karma/validation');
describe('karmaConfigValidation', () => {
describe('ensureWebpackFrameworkExists', () => {

View file

@ -0,0 +1,8 @@
const DefaultWebpackOptionsFactory = require('../../../lib/webpack/defaults');
describe('DefaultWebpackOptionsFactory', () => {
it('should be defined', () => {
expect(DefaultWebpackOptionsFactory).toBeDefined();
// todo(mikol): write a DefaultWebpackOptionsFactory test suite before v5 official release.
});
});

View file

@ -0,0 +1,8 @@
const KW_WebpackPlugin = require('../../../lib/webpack/plugin');
describe('KW_WebpackPlugin', () => {
it('should be defined', () => {
expect(KW_WebpackPlugin).toBeDefined();
// todo(mikol): write a KW_WebpackPlugin test suite before v5 official release.
});
});