Skip to content

Commit

Permalink
feat: add webpack as framework; use shared bundles
Browse files Browse the repository at this point in the history
BREAKING CHANGE: webpack needs to be added to frameworks

```
// old:
frameworks: ['mocha'],

// new:
frameworks: ['mocha', 'webpack'],
```

BREAKING CHANGE: old alternative usage is no longer recommended
BREAKING CHANGE: webpack-dev-middleware removed
BREAKING CHANGE: default webpack configuration changed drastically
  • Loading branch information
daKmoR committed Dec 12, 2018
1 parent e207fe5 commit 997c955
Show file tree
Hide file tree
Showing 12 changed files with 3,231 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
123 changes: 65 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,79 @@ 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`

This configuration is more performant, but you cannot run single test anymore (only the complete suite).
### Default webpack configuration

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.
This configuration will be merged with what gets provided via karma's config.webpack.

**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.

The first preproccessor triggers the build of all the bundles/chunks and all following files just return the output of this one build process.

### Webpack typescript support

By default karma-webpack forces *.js files so if you test *.ts files and use webpack to build typescript to javascript it works out of the box.

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.
If you have a different need you can override by settig `webpack.transformPath`

```js
// this is the by default applied transformPath
webpack: {
transformPath: (filepath) => {
// force *.js files by default
const info = path.parse(filepath);
return `${path.join(info.dir, info.name)}.js`;
},
},
```

### `Source Maps`

Expand Down Expand Up @@ -126,34 +156,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 997c955

Please sign in to comment.