Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dferber90 committed Sep 22, 2018
0 parents commit b0f71d1
Show file tree
Hide file tree
Showing 7 changed files with 645 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 Dominik Ferber

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# jest-transform-css

A Jest transfomer which enables importing CSS into Jest's `jsdom`.

**If you are not here for Visual Regression Testing, but just want to make your tests work with CSS Modules, then you are likley looking for https://github.com/keyanzhang/identity-obj-proxy/.**

> ⚠️ **This package is experimental.**
> It works with the tested project setups, but needs to be tested in more.
> If you struggle to set it up properly, it might be the fault of this package.
> Please file an issue and provide reproduction, or even open a PR to add support.
>
> The document is also sparse at the moment. Feel free to open an issue in case you have any questions!
>
> I am not too familiar with PostCSS and Jest, so further simplification of
> this plugin might be possible. I'd appreciate any hints!
>
> If this approach is working for you, please let me know on Twitter ([@dferber90](https://twitter.com/dferber90)) or by starring the [GitHub repo](https://github.com/dferber90/jest-transform-css).
>
> I am looking for contributors to help improve this package!
## Description

When you want to do Visual Regression Testing in Jest, it is important that the CSS of components is available to the test setup. So far, CSS was not part of tests as it was mocked away by `identity-obj-proxy`.

`jest-transform-css` is inteded to be used in an `jsdom` environment. When any component imports CSS in the test environment, then the loaded CSS will get added to `jsdom` using [`style-inject`](https://github.com/egoist/style-inject) - just like the Webpack CSS loader would do in a production environment. This means the full styles are added to `jsdom`.

This doesn't make much sense at first, as `jsdom` is headless (non-visual). However, we can copy the resulting document markup ("the HTML") of `jsdom` and copy it to a [`puppeteer`](https://github.com/googlechrome/puppeteer/) instance. We can let the markup render there and take a screenshot there. The [`jsdom-screenshot`](https://github.com/dferber90/jsdom-screenshot) package does exactly this.

Once we obtained a screenshot, we can compare it to the last version of that screenshot we took, and make tests fail in case they did. The [`jest-image-snapshot`](https://github.com/americanexpress/jest-image-snapshot) plugin does that.

## Installation

```bash
yarn add jest-transform-css --dev
```

## Setup

### Setup - adding `transform`

Open `jest.config.js` and modify the `transform`:

```
transform: {
"^.+\\.js$": "babel-jest",
"^.+\\.css$": "./jest-transform-css"
}
```

> Notice that `babel-jest` gets added as well.
>
> The `babel-jest` code preprocessor is enabled by default, when no other preprocessors are added. As `jest-transform-css` is a code preprocessor, `babel-jest` gets disabled when `jest-transform-css` is added.
>
> So it needs to be added again manually.
>
> See https://github.com/facebook/jest/tree/master/packages/babel-jest#setup
### Setup - removing `identity-obj-proxy`

If your project is using CSS Modules, then it's likely that `identity-obj-proxy` is configured. It needs to be removed in order for the styles of the `jest-transform-css` to apply.

So, remove these lines from `jest.config.js`:

```diff
"moduleNameMapper": {
- "\\.(s?css|less)$": "identity-obj-proxy"
},
```

## Further setup

There are many ways to setup styles in a project (CSS modules, global styles, external global styles, local global styles, CSS in JS, LESS, SASS just to name a few). How to continue from here on depends on your project.

### Further Setup - PostCSS

If your setup is using `PostCSS` then you should add a `postcss.config.js` at the root of your folder.

You can apply certain plugins only when `process.env.NODE_ENV === 'test'`. Ensure that valid CSS can be generated. It might be likely that more functionality (transforms) are required to make certain CSS work (like background-images).

### Further Setup - css-loader

If your setup is using `css-loader` only, without PostCSS then you should be fine. When components import CSS in the test environment, then the CSS is transformed through PostCSS's `cssModules` plugin to generate the classnames. It also injects the styles into `jsdom`.

## Known Limitations

At the moment I struggled to get CSS from `node_modules` to transpile, due to the `jest` configuration. I might just be missing something obvious.
62 changes: 62 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require("fs");
const crossSpawn = require("cross-spawn");
const stripIndent = require("common-tags/lib/stripIndent");

module.exports = {
process: (src, filename, config, options) => {
// The "process" function of this Jest transform must be sync,
// but postcss is async. So we spawn a sync process to do an sync
// transformation!
// https://twitter.com/kentcdodds/status/1043194634338324480
const postcssRunner = `${__dirname}/postcss-runner.js`;
const result = crossSpawn.sync("node", [
"-e",
stripIndent`
require("${postcssRunner}")(
${JSON.stringify({
src,
filename
// config,
// options
})}
)
.then(out => { console.log(JSON.stringify(out)) })
`
]);

// check for errors of postcss-runner.js
const error = result.stderr.toString();
if (error) throw error;

// read results of postcss-runner.js from stdout
let css;
let tokens;
try {
// we likely logged something to the console from postcss-runner
// in order to debug, and hence the parsing fails!
parsed = JSON.parse(result.stdout.toString());
css = parsed.css;
tokens = parsed.tokens;
if (Array.isArray(parsed.warnings))
parsed.warnings.forEach(warning => {
console.warn(warning);
});
} catch (error) {
// we forward the logs and return no mappings
console.error(result.stderr.toString());
console.log(result.stdout.toString());
return stripIndent`
console.error("transform-css: Failed to load '${filename}'");
module.exports = {};
`;
}

// Finally, inject the styles to the document
return stripIndent`
const styleInject = require('style-inject');
styleInject(${JSON.stringify(css)});
module.exports = ${JSON.stringify(tokens)};
`;
}
};
21 changes: 21 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "jest-transform-css",
"description": "Jest transfomer to import CSS into Jest's `jsdom`",
"version": "0.0.1",
"main": "index.js",
"author": "Dominik Ferber <[email protected]> (http://dferber.de/)",
"license": "MIT",
"dependencies": {
"common-tags": "1.8.0",
"cross-spawn": "6.0.5",
"postcss": "7.0.2",
"postcss-load-config": "2.0.0",
"postcss-modules": "1.3.2",
"style-inject": "0.3.0"
},
"keywords": [
"postcss-runner",
"jest",
"css"
]
}
72 changes: 72 additions & 0 deletions postcss-runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const postcss = require("postcss");
const postcssrc = require("postcss-load-config");
const cssModules = require("postcss-modules");

// This script is essentially a PostCSS Runner
// https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#postcss-runner-guidelines
module.exports = ({ src, filename }) => {
const ctx = {
// Not sure whether the map is useful or not.
// Disabled for now. We can always enable it once it becomes clear.
map: false,
// To ensure that PostCSS generates source maps and displays better syntax
// errors, runners must specify the from and to options. If your runner does
// not handle writing to disk (for example, a gulp transform), you should
// set both options to point to the same file"
// https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#21-set-from-and-to-processing-options
from: filename,
to: filename
};
let tokens = {};
return postcssrc(ctx)
.then(
config => ({ ...config, plugins: config.plugins || [] }),
error => {
// Support running without postcss.config.js
// This is useful in case the webpack setup of the consumer does not
// use PostCSS at all and simply uses css-loader in modules mode.
if (error.message.startsWith("No PostCSS Config found in:")) {
return { plugins: [], options: { from: filename, to: filename } };
}
throw error;
}
)
.then(({ plugins, options }) => {
return postcss([
cssModules({
// Should we read generateScopedName from options?
// Does anybody care about the actual names? This is test-only anyways?
// Should be easy to add in case anybody needs it, just pass it through
// from jest.config.js (we have "config" & "options" in css.js)
generateScopedName: "[path][local]-[hash:base64:10]",
getJSON: (cssFileName, exportedTokens, outputFileName) => {
tokens = exportedTokens;
}
}),
...plugins
])
.process(src, options)
.then(
result => ({
css: result.css,
tokens,
// Display result.warnings()
// PostCSS runners must output warnings from result.warnings()
// https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#32-display-resultwarnings
warnings: result.warnings().map(warn => warn.toString())
}),
// Don’t show JS stack for CssSyntaxError
// PostCSS runners must not show a stack trace for CSS syntax errors,
// as the runner can be used by developers who are not familiar with
// JavaScript. Instead, handle such errors gracefully:
// https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#31-dont-show-js-stack-for-csssyntaxerror
error => {
if (error.name === "CssSyntaxError") {
process.stderr.write(error.message + error.showSourceCode());
} else {
throw error;
}
}
);
});
};
Loading

0 comments on commit b0f71d1

Please sign in to comment.