Skip to content

Commit

Permalink
feat: add webpack as framework; use shared bundles
Browse files Browse the repository at this point in the history
  • Loading branch information
daKmoR committed Dec 12, 2018
1 parent e207fe5 commit 733dbe1
Show file tree
Hide file tree
Showing 12 changed files with 3,214 additions and 3,399 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.eslintcache

lib
coverage
node_modules

Expand Down
106 changes: 48 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ npm i -D karma-webpack
module.exports = (config) => {
config.set({
// ... normal karma configuration

// add webpack to your list of frameworks
frameworks: ['mocha', 'webpack'],

files: [
// all files ending in "_test"
{ pattern: 'test/*_test.js', watched: false },
{ pattern: 'test/**/*_test.js', watched: false }
// each file acts as entry point for the webpack configuration
// all files ending in ".test.js"
'test/**/*.test.js',
],

preprocessors: {
Expand All @@ -46,51 +48,62 @@ module.exports = (config) => {

webpack: {
// karma watches the test entry points
// (you don't need to specify the entry option)
// Do NOT specify the entry option
// webpack watches dependencies

// webpack configuration
},

webpackMiddleware: {
// webpack-dev-middleware configuration
// i. e.
stats: 'errors-only'
}
})
});
}
```

### `Alternative Usage`
### Default webpack configuration

This configuration is more performant, but you cannot run single test anymore (only the complete suite).
This configuration will be merged with what get's provided via karmas config.webpack.

The above configuration generates a `webpack` bundle for each test. For many test cases this can result in many big files. The alternative configuration creates a single bundle with all test cases.

**karma.conf.js**
```js
files: [
// only specify one entry point
// and require all tests in there
'test/index_test.js'
],

preprocessors: {
// add webpack as preprocessor
'test/index_test.js': [ 'webpack' ]
},
const defaultWebpackOptions = {
mode: 'development',
output: {
filename: '[name].js',
path: path.join(os.tmpdir(), '_karma_webpack_'),
},
stats: {
modules: false,
colors: true,
},
watch: false,
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
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()
// ],
};
```

**test/index_test.js**
```js
// require all modules ending in "_test" from the
// current directory and all subdirectories
const testsContext = require.context(".", true, /_test$/)
### How it works

testsContext.keys().forEach(testsContext)
```
This project is a framework and preprocessor for Karma that combines test files and dependencies into 2 shared bundles and 1 chunk per test file. It relies on webpack to generate the bundles/chunks and to keep it updated during autoWatch=true.

Every test file is required using the [require.context](https://webpack.js.org/guides/dependency-management/#require-context) and compiled with webpack into one test bundle.
The first preproccessor triggers the build of all the bundles/chunks and all following files just return the output of this one build process.

### `Source Maps`

Expand Down Expand Up @@ -126,34 +139,11 @@ This is the full list of options you can specify in your `karma.conf.js`
|Name|Type|Default|Description|
|:--:|:--:|:-----:|:----------|
|[**`webpack`**](#webpack)|`{Object}`|`{}`|Pass `webpack.config.js` to `karma`|
|[**`webpackMiddleware`**](#webpackmiddleware)|`{Object}`|`{}`|Pass `webpack-dev-middleware` configuration to `karma`|
|[**`beforeMiddleware`**](#beforemiddleware)|`{Object}`|`{}`|Pass custom middleware configuration to `karma`, **before** any `karma` middleware runs|

### `webpack`

`webpack` configuration (`webpack.config.js`).

### `webpackMiddleware`

Configuration for `webpack-dev-middleware`.

### `beforeMiddleware`

`beforeMiddleware` is a `webpack` option that allows injecting middleware before
karma's own middleware runs. This loader provides a `webpackBlocker`
middleware that will block tests from running until code recompiles. That is,
given this scenario

1. Have a browser open on the karma debug page (http://localhost:9876/debug.html)
2. Make a code change
3. Refresh

Without the `webpackBlocker` middleware karma will serve files from before
the code change. With the `webpackBlocker` middleware the loader will not serve
the files until the code has finished recompiling.

> **⚠️ The `beforeMiddleware` option is only supported in `karma >= v1.0.0`**
<h2 align="center">Maintainers</h2>

<table>
Expand Down
181 changes: 181 additions & 0 deletions lib/KarmaWebpackController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/* eslint-disable no-console */

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;
}

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',
path: path.join(os.tmpdir(), '_karma_webpack_'),
},
stats: {
modules: false,
colors: true,
},
watch: false,
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
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;
}

get webpackOptions() {
return this.__webpackOptions;
}

set karmaEmitter(emitter) {
this.__karmaEmitter = emitter;

this.__webpackOptions.plugins.push(
new KarmaSyncPlugin({
karmaEmitter: emitter,
controller: this,
})
);

emitter.on('exit', (done) => {
this.onKarmaExit();
done();
});
}

get karmaEmitter() {
return this.__karmaEmitter;
}

get outputPath() {
return this.webpackOptions.output.path;
}

constructor() {
this.isActive = false;
this.bundlesContent = {};
this.__debounce = false;
this.webpackOptions = defaultWebpackOptions;
}

updateWebpackOptions(newOptions) {
this.webpackOptions = merge(this.webpackOptions, newOptions);
}

async bundle() {
if (this.isActive === false && this.__debounce === false) {
console.log('Webpack bundling...');
this._activePromise = this._bundle();
}
return this._activePromise;
}

async _bundle() {
this.isActive = true;
this.__debounce = true;
this.compiler = webpack(this.webpackOptions);
return new Promise((resolve) => {
if (this.webpackOptions.watch === true) {
console.log('Webpack starts watching...');
this.webpackFileWatcher = this.compiler.watch({}, (err, stats) =>
this.handleBuildResult(err, stats, resolve)
);
} else {
this.compiler.run((err, stats) =>
this.handleBuildResult(err, stats, resolve)
);
}
});
}

handleBuildResult(err, stats, resolve) {
if (err) {
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
return;
}

const info = stats.toJson();
if (stats.hasErrors()) {
console.error(info.errors);
}
if (stats.hasWarnings()) {
console.warn(info.warnings);
}

this.__debounce = setTimeout(() => (this.__debounce = false), 100);
this.isActive = false;

console.log(stats.toString(this.webpackOptions.stats));
resolve();
}

onKarmaExit() {
if (this.webpackFileWatcher) {
this.webpackFileWatcher.close();
console.log('Webpack stopped watching.');
}
}
}

module.exports = {
KarmaSyncPlugin,
KarmaWebpackController,
defaultWebpackOptions,
};
File renamed without changes.
Loading

0 comments on commit 733dbe1

Please sign in to comment.