import React from 'react' import { shallow } from 'enzyme' import { resetStyled, expectCSSMatches, seedNextClassnames } from './utils' import _injectGlobal from '../constructors/injectGlobal' import stringifyRules from '../utils/stringifyRules' import css from '../constructors/css' import _keyframes from '../constructors/keyframes' import StyleSheet from '../models/StyleSheet' import { SC_ATTR } from '../constants' const keyframes = _keyframes(hash => `keyframe_${hash%1000}`, stringifyRules, css) const injectGlobal = _injectGlobal(stringifyRules, css) const getStyleTags = () => ( Array.from(document.querySelectorAll('style')).map(el => ({ css: el.innerHTML.trim().replace(/\s+/mg, ' ') })) ) let styled describe('rehydration', () => { /** * Make sure the setup is the same for every test */ beforeEach(() => { styled = resetStyled() }) describe('with existing styled components', () => { beforeEach(() => { document.head.innerHTML = ` ` StyleSheet.reset() }) it('should preserve the styles', () => { expectCSSMatches('.TWO {} .b { color: red; }') }) it('should append a new component like normal', () => { const Comp = styled.div.withConfig({ componentId: 'ONE' })` color: blue; ` shallow() expectCSSMatches('.b { color: red; } .ONE { } .a { color:blue; }') }) it('should reuse a componentId', () => { const A = styled.div.withConfig({ componentId: 'ONE' })`color: blue;` shallow() const B = styled.div.withConfig({ componentId: 'TWO' })`` shallow() expectCSSMatches('.b { color: red; } .ONE { } .a { color:blue; }') }) it('should reuse a componentId and generated class', () => { const A = styled.div.withConfig({ componentId: 'ONE' })`color: blue;` shallow() const B = styled.div.withConfig({ componentId: 'TWO' })`color: red;` shallow() expectCSSMatches('.b { color: red; } .ONE { } .a { color:blue; }') }) it('should reuse a componentId and inject new classes', () => { const A = styled.div.withConfig({ componentId: 'ONE' })`color: blue;` shallow() const B = styled.div.withConfig({ componentId: 'TWO' })`color: red;` shallow() const C = styled.div.withConfig({ componentId: 'TWO' })`color: green;` shallow() expectCSSMatches('.b{ color: red; } .c{ color:green; } .ONE { } .a{ color:blue; }') }) }) describe('with styled components with props', () => { beforeEach(() => { /* Hash 1323611362 is based on name TWO and contents color: red. * Change either and this will break. */ document.head.innerHTML = ` ` StyleSheet.reset() }) it('should preserve the styles', () => { expectCSSMatches(` .ONE { } .a { color: blue; } .TWO { } .b { color: red; } `) }) it('should not inject new styles for a component already rendered', () => { const Comp = styled.div.withConfig({ componentId: 'ONE' })` color: ${ props => props.color }; ` shallow() expectCSSMatches(` .ONE { } .a { color: blue; } .TWO { } .b { color: red; } `) }) it('should inject new styles for a new computed style of a component', () => { seedNextClassnames(['x']) const Comp = styled.div.withConfig({ componentId: 'ONE' })` color: ${ props => props.color }; ` shallow() expectCSSMatches(` .a { color: blue; } .x { color:green; } .b { color: red; } `) }) }) describe('with inline styles that werent rendered by us', () => { beforeEach(() => { /* Same css as before, but without the data attributes we ignore it */ document.head.innerHTML = ` ` StyleSheet.reset() }) it('should leave the existing styles there', () => { expectCSSMatches('.TWO {} .b { color: red; }') }) it('should generate new classes, even if they have the same name', () => { const A = styled.div.withConfig({ componentId: 'ONE' })`color: blue;` shallow() const B = styled.div.withConfig({ componentId: 'TWO' })`color: red;` shallow() expectCSSMatches('.TWO {} .b { color: red; } .ONE { } .a { color:blue; } .TWO {} .b { color:red; } ') }) }) describe('with global styles', () => { beforeEach(() => { /* Adding a non-local stylesheet with a hash 557410406 which is * derived from "body { background: papayawhip; }" so be careful * changing it. */ document.head.innerHTML = ` ` StyleSheet.reset() }) it('should leave the existing styles there', () => { expectCSSMatches('body { background: papayawhip; } .TWO {} .b { color: red; }') }) it('should inject new global styles at the end', () => { injectGlobal` body { color: tomato; } ` expectCSSMatches('body { background: papayawhip; } .b { color: red; } body { color:tomato; }') }) it('should interleave global and local styles', () => { injectGlobal` body { color: tomato; } ` const A = styled.div.withConfig({ componentId: 'ONE' })`color: blue;` shallow() expectCSSMatches('body { background: papayawhip; } .b { color: red; } body { color:tomato; } .ONE { } .a { color:blue; }') expect(getStyleTags()).toEqual([ { css: '/* sc-component-id: sc-global-557410406 */ body{background:papayawhip;} ', }, { css: '/* sc-component-id: TWO */ .b{color:red;} ', }, { css: '/* sc-component-id: sc-global-2299393384 */ body{color:tomato;} ', }, { css: '/* sc-component-id: ONE */ .ONE {} .a{color:blue;}', }, ].reduce((acc, { css }) => { acc[0].css += css return acc }, [{ css: '' }])) }) }) describe('with all styles already rendered', () => { let styleTags beforeEach(() => { document.head.innerHTML = ` ` styleTags = Array.from(document.querySelectorAll('style')) StyleSheet.reset() }) it('should not touch existing styles', () => { expectCSSMatches(` html { font-size: 16px; } body { background: papayawhip; } .ONE { } .a { color: blue; } .TWO { } .b { color: red; } `) }) it('should replace stylesheets on-demand', () => { const tagsAfterReset = Array.from(document.querySelectorAll('style')) expect(tagsAfterReset[0]).toBe(styleTags[0]) expect(tagsAfterReset[1]).toBe(styleTags[1]) /* Rerendering existing tags doesn't touch the DOM */ const A = styled.div.withConfig({ componentId: 'ONE' })`color: blue;` shallow() const B = styled.div.withConfig({ componentId: 'TWO' })`color: red;` shallow() const styleTagsAfterRehydration = Array.from(document.querySelectorAll('style')) expect(styleTagsAfterRehydration[0]).toEqual(styleTags[0]) /* Only when new components are introduced (or a previous component * generates a new hash) does the style tag get replaced. */ const C = styled.div.withConfig({ componentId: 'THREE' })`color: green;` shallow() /* the order stays correct and the styles are unharmed * NOTE: during rehydration the empty rules are stripped out however */ expectCSSMatches(` html { font-size:16px; } body { background:papayawhip; } .a { color:blue; } .b { color:red; } .THREE { } .c { color:green; } `) }) it('should not change styles if rendered in the same order they were created with', () => { injectGlobal` html { font-size: 16px; } ` injectGlobal` body { background: papayawhip; } ` const A = styled.div.withConfig({ componentId: 'ONE' })`color: blue;` shallow() const B = styled.div.withConfig({ componentId: 'TWO' })`color: red;` shallow() expectCSSMatches(` html { font-size: 16px; } body { background: papayawhip; } .ONE { } .a { color: blue; } .TWO { } .b { color: red; } `) }) it('should still not change styles if rendered in a different order', () => { const B = styled.div.withConfig({ componentId: 'TWO' })`color: red;` shallow() injectGlobal` body { background: papayawhip; } ` const A = styled.div.withConfig({ componentId: 'ONE' })`color: blue;` shallow() injectGlobal` html { font-size: 16px; } ` expectCSSMatches(` html { font-size: 16px; } body { background: papayawhip; } .ONE { } .a { color: blue; } .TWO { } .b { color: red; } `) }) }) describe('with keyframes', () => { beforeEach(() => { document.head.innerHTML = ` ` StyleSheet.reset() }) it('should not touch existing styles', () => { expectCSSMatches(` @-webkit-keyframes keyframe_880 {from {opacity: 0;}}@keyframes keyframe_880 {from {opacity: 0;}} `) }) it('should not regenerate keyframes', () => { keyframes` from { opacity: 0; } ` expectCSSMatches(` @-webkit-keyframes keyframe_880 {from {opacity: 0;}}@keyframes keyframe_880 {from {opacity: 0;}} `) }) it('should still inject new keyframes', () => { keyframes` from { opacity: 1; } ` expectCSSMatches(` @-webkit-keyframes keyframe_880 {from {opacity: 0;}}@keyframes keyframe_880 {from {opacity: 0;}} @-webkit-keyframes keyframe_144 {from {opacity:1;}}@keyframes keyframe_144 {from {opacity:1;}} `) }) it('should pass the keyframes name along as well', () => { const fadeIn = keyframes` from { opacity: 0; } ` const A = styled.div`animation: ${fadeIn} 1s both;` const fadeOut = keyframes` from { opacity: 1; } ` const B = styled.div`animation: ${fadeOut} 1s both;` /* Purposely rendering out of order to make sure the output looks right */ shallow() shallow() expectCSSMatches(` @-webkit-keyframes keyframe_880 {from {opacity: 0;}}@keyframes keyframe_880 {from {opacity: 0;}} .sc-a { } .d { -webkit-animation:keyframe_880 1s both; animation:keyframe_880 1s both; } @-webkit-keyframes keyframe_144 {from {opacity:1;}}@keyframes keyframe_144 {from {opacity:1;}} .sc-b { } .c { -webkit-animation:keyframe_144 1s both; animation:keyframe_144 1s both; } `) }) }) })