Node Hook Filename
Importing assets is cool
If you've ever imported an asset into your node based browser project - you'll know if feels super cool. Webpack is one great bundler that makes this possible.
import src from './brand.jpg'
const Logo = (props) => <img {...props} src={src} />
// throw a party! ♪┏(^.^)┛┗(^.^)┓┗(^.^)┛┏(^.^)┓ ♪
Due to configuration (not shown). The above code is importing a path string to an asset which is used when rendering a logo. This works great inside a Webpack bundle, but we don't necessarily want to bundle our tests! So how can we get it working in tests and better yet, write meaning full assertions?
Webpack-Isomorphic-Tools, not here
There are a few solutions to this, one big one is Webpack-isomorphic-tools which can be used to map asset files to configurable values. But this relies on an initial Webpack build generating a JSON file, so doesn't really work for testing.
Node-Hook-Filename
This is a simple module that intercepts node's require calls. Given an array of strings, it will test each require
against those strings and when a match is found, the result of requiring will be the filename.
require('node-hook-filename')(['foo'])
const foo = require('path/to/foo')
// foo will now be a string of `something/foo`,
// regardless of what exists in the foo file.
const bar = require('path/to/bar')
// bar is unaffected and behaves as normal
Above is a simple example of how we can alter require statements and prevent node from reading source files. We can use this! If we put the node-hook-filename
call only in our test environment set-up it can be used to assist with assertions against assets!
So for CSS-modules, the hook changing the export to a string isn't enough, we need an object with some classNames!
An example
So, say we have the same source file as the start of this post, a simple block that makes use of some SCSS modules for classNames.
import src from './brand.jpg'
const Logo = (props) => <img {...props} src={src} />
For testing this, we don't want to use Webpack, we just want to test that the correct image is used for the logo!
// test.setup.js
// hook out all the .jpg imports
require('node-hook-filename')([ '.jpg' ])
// Logo.spec.js
import brandImg from './brand.jpg'
// brandImg === './brand.jpg'
it('sets the img.src as the brand image', () => {
expect(shallow(<Logo />)).to.have.prop('src', brandImg)
})
By using a node hook for an extension, we have stopped the test code from trying to read an image (which would make it barf). And, we have still got a value we can test against.
More Complex Examples
For CSS-Modules, a string value of the filename isn't enough -- we need an object containing keys that match the appropriate class names. For this, we can provide an implementation for generating the return value.
This is a great excuse to play with proxies! We can proxy the getter of an object to a useful value, such as an string containing the name of the key being called!
NB: Proxies don't exist in node yet... but they're coming in Node version 6! Right now you will have to run these tests in a browser based test runner.
Here's what that might look like:
require('node-hook-filename')([ '.module.scss' ], (filename) => {
const styleObject = { }
return new Proxy(styleObject, {
get(target, key) {
return `${filename}__${key}`
},
})
})
Here, we've created a hook for files with an extension .module.scss
. The hooked out require calls will now return an empty object, styleObject
, wrapped in a proxy. This proxy has defined a custom getter that will now return a string made up of the filename and the key being called on the object.
import styles from './anything.module.scss'
styles.foo // "./anything.module.scss__foo"
styles.className // "./anything.module.scss__className"
styles.anything // "./anything.module.scss__anything"
So now, no matter what property is accessed on the object, we're using a proxy to modify the value to something we can easily test!
Stay tuned for more posts!
- tomatao