Skip to content

Instantly share code, notes, and snippets.

@rszewczyk
Created April 6, 2018 16:34
Show Gist options
  • Save rszewczyk/4375363cddff6636262d76d8a073b860 to your computer and use it in GitHub Desktop.
Save rszewczyk/4375363cddff6636262d76d8a073b860 to your computer and use it in GitHub Desktop.
Emotion jest snapshot serializer for DOM elements
// proof of concept for jest-emotion style snapshots for DOM Elements
import * as css from 'css'
function defaultClassNameReplacer(className, index) {
return `emotion-${index}`
}
const componentSelectorClassNamePattern = /\.e[a-zA-Z0-9-]+[0-9]+/
export const replaceClassNames = (
selectors,
styles,
code,
key,
replacer = defaultClassNameReplacer
) => {
let index = 0
const classRegex = new RegExp(`^\\.${key}-([a-zA-Z0-9-]+)`)
return selectors.reduce((acc, className) => {
if (
classRegex.test(className) ||
componentSelectorClassNamePattern.test(className)
) {
const escapedRegex = new RegExp(
className.replace('.', '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'),
'g'
)
return acc.replace(escapedRegex, replacer(className, index++))
}
return acc
}, `${styles}${styles ? '\n\n' : ''}${code}`)
}
function getNodes(node, nodes = []) {
if (node.children) {
for (let child of node.children) {
getNodes(child, nodes)
}
}
nodes.push(node)
return nodes
}
function markNodes(nodes) {
nodes.forEach(node => {
node.withEmotionStyles = true
})
}
function getSelectors(nodes) {
return nodes.reduce((selectors, node) => {
const classes = node.getAttribute('class')
return classes
? selectors.concat(classes.split(' ').map(c => `.${c}`))
: selectors
}, [])
}
const ELEMENT_NODE = 1
const ELEMENT_REGEXP = /^((HTML|SVG)\w*)?Element$/
function testNode(nodeType, name) {
return nodeType === ELEMENT_NODE && ELEMENT_REGEXP.test(name)
}
function test(val) {
return (
val &&
!val.withEmotionStyles &&
val.constructor &&
val.constructor.name &&
testNode(val.nodeType, val.constructor.name)
)
}
export function getStyles(emotion) {
return Object.keys(emotion.caches.inserted).reduce((style, current) => {
if (emotion.caches.inserted[current] === true) {
return style
}
return style + emotion.caches.inserted[current]
}, '')
}
function filterChildSelector(baseSelector) {
if (baseSelector.slice(-1) === '>') {
return baseSelector.slice(0, -1)
}
return baseSelector
}
function getStylesFromSelectors(nodeSelectors, emotion) {
const styles = getStyles(emotion)
let ast
try {
ast = css.parse(styles)
} catch (e) {
console.error(e)
throw new Error(
`There was an error parsing css in jest-emotion-react: "${styles}"`
)
}
ast.stylesheet.rules = ast.stylesheet.rules.reduce(reduceRules, [])
const ret = css.stringify(ast)
return ret
function reduceRules(rules, rule) {
let shouldIncludeRule = false
if (rule.type === 'rule') {
shouldIncludeRule = rule.selectors.some(selector => {
const baseSelector = filterChildSelector(
selector.split(/:| |\./).filter(s => !!s)[0]
)
return nodeSelectors.some(
sel => sel === baseSelector || sel === `.${baseSelector}`
)
})
}
if (rule.type === 'media' || rule.type === 'supports') {
rule.rules = rule.rules.reduce(reduceRules, [])
if (rule.rules.length) {
shouldIncludeRule = true
}
}
return shouldIncludeRule ? rules.concat(rule) : rules
}
}
export function createSerializer(emotion, { classNameReplacer } = {}) {
function print(val, printer) {
const nodes = getNodes(val)
markNodes(nodes)
const selectors = getSelectors(nodes)
const styles = getStylesFromSelectors(selectors, emotion)
const printedVal = printer(val)
return replaceClassNames(
selectors,
styles,
printedVal,
emotion.caches.key,
classNameReplacer
)
}
return { print, test }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment