UNPKG

aphrodite

Version:

Framework-agnostic CSS-in-JS with support for server-side rendering, browser prefixing, and minimum CSS generation

671 lines (543 loc) 21.7 kB
import asap from 'asap'; import {assert} from 'chai'; import {JSDOM} from 'jsdom'; import { StyleSheet, css, minify } from '../src/index'; import { injectAndGetClassName, injectStyleOnce, reset, resetInjectedStyle, startBuffering, flushToString, flushToStyleTag, addRenderedClassNames, getRenderedClassNames, } from '../src/inject'; import { defaultSelectorHandlers } from '../src/generate'; import { getSheetText } from './testUtils'; const sheet = StyleSheet.create({ red: { color: 'red', }, blue: { color: 'blue', }, green: { color: 'green', }, }); describe('injection', () => { beforeEach(() => { global.document = new JSDOM('').window.document; reset(); }); afterEach(() => { global.document.close(); global.document = undefined; }); describe('injectAndGetClassName', () => { it('uses hashed class name', () => { const className = injectAndGetClassName(false, [sheet.red], defaultSelectorHandlers); assert.equal(className, 'red_137u7ef'); }); it('combines class names', () => { const className = injectAndGetClassName(false, [sheet.red, sheet.blue, sheet.green], defaultSelectorHandlers); assert.equal(className, 'red_137u7ef-o_O-blue_1tsdo2i-o_O-green_1jzdmtb'); }); it('ignores null values in styleDefinitions', () => { const className = injectAndGetClassName(false, [sheet.red, sheet.blue, null], defaultSelectorHandlers); assert.equal(className, 'red_137u7ef-o_O-blue_1tsdo2i'); }); describe('process.env.NODE_ENV === \'production\'', () => { let prodSheet; beforeEach(() => { process.env.NODE_ENV = 'production'; minify(true); prodSheet = StyleSheet.create({ red: { color: 'red', }, blue: { color: 'blue', }, green: { color: 'green', }, }); }); afterEach(() => { delete process.env.NODE_ENV; minify(false); }); it('uses hashed class name (does not re-hash)', () => { const className = injectAndGetClassName(false, [prodSheet.red], defaultSelectorHandlers); assert.equal(className, `_${prodSheet.red._name}`); }); it('creates minified combined class name', () => { const className = injectAndGetClassName(false, [prodSheet.red, prodSheet.blue, prodSheet.green], defaultSelectorHandlers); assert.equal(className, '_11v1eztc'); }); it('ignores null values in styleDefinitions', () => { const className = injectAndGetClassName(false, [ null, prodSheet.red, null, prodSheet.blue, prodSheet.green, null ], defaultSelectorHandlers); assert.equal(className, '_11v1eztc'); }); }); }); describe('injectStyleOnce', () => { it('causes styles to automatically be added', done => { injectStyleOnce("x", ".x", [{ color: "red" }], false); asap(() => { const styleTags = global.document.getElementsByTagName("style"); assert.equal(styleTags.length, 1); const styles = getSheetText(styleTags[0].sheet); assert.include(styles, ".x {"); assert.include(styles, "color: red"); done(); }); }); it('causes styles to be added async, and buffered', done => { injectStyleOnce("x", ".x", [{ color: "red" }], false); const styleTags = global.document.getElementsByTagName("style"); assert.equal(styleTags.length, 0); injectStyleOnce("y", ".y", [{ color: "blue" }], false); asap(() => { const styleTags = global.document.getElementsByTagName("style"); assert.equal(styleTags.length, 1); const styles = getSheetText(styleTags[0].sheet); assert.include(styles, ".x {"); assert.include(styles, ".y {"); assert.include(styles, "color: red"); assert.include(styles, "color: blue"); done(); }); }); it('doesn\'t inject the same style twice', done => { injectStyleOnce("x", ".x", [{ color: "red" }], false); injectStyleOnce("x", ".x", [{ color: "blue" }], false); asap(() => { const styleTags = global.document.getElementsByTagName("style"); assert.equal(styleTags.length, 1); const styles = getSheetText(styleTags[0].sheet); assert.include(styles, ".x {"); assert.include(styles, "color: red"); assert.notInclude(styles, "color: blue"); assert.equal(styles.match(/\.x {/g).length, 1); done(); }); }); it('throws an error if we\'re not buffering and on the server', () => { const oldDocument = global.document; global.document = undefined; assert.throws(() => { injectStyleOnce("x", ".x", [{ color: "red" }], false); }, "Cannot automatically buffer"); global.document = oldDocument; }); // browser-specific tests it('adds to .innerText if insertRule is not available', done => { const styleTag = global.document.createElement("style"); styleTag.setAttribute("data-aphrodite", ""); document.head.appendChild(styleTag); styleTag.sheet.insertRule = null; injectStyleOnce("x", ".x", [{ color: "red" }], false); asap(() => { assert.include(styleTag.innerText, ".x{"); assert.include(styleTag.innerText, "color:red"); done(); }); }); it('uses document.getElementsByTagName without document.head', done => { Object.defineProperty(global.document, "head", { value: null, }); injectStyleOnce("x", ".x", [{ color: "red" }], false); asap(() => { const styleTags = global.document.getElementsByTagName("style"); assert.equal(styleTags.length, 1); const styles = getSheetText(styleTags[0].sheet); assert.include(styles, ".x {"); assert.include(styles, "color: red"); done(); }); }); }); describe('startBuffering', () => { it('causes styles to not be added automatically', done => { startBuffering(); css(sheet.red); asap(() => { const styleTags = global.document.getElementsByTagName("style"); assert.equal(styleTags.length, 0); done(); }); }); it('throws an error if we try to buffer twice', () => { startBuffering(); assert.throws(() => { startBuffering(); }, "already buffering"); }); }); describe('flushToStyleTag', () => { it('adds a style tag with all the buffered styles', () => { startBuffering(); css(sheet.red); css(sheet.blue); flushToStyleTag(); const styleTags = global.document.getElementsByTagName("style"); const lastTag = styleTags[styleTags.length - 1]; const styles = getSheetText(lastTag.sheet); assert.include(styles, `.${sheet.red._name} {`); assert.include(styles, `.${sheet.blue._name} {`); assert.match(styles, /color: red/); assert.match(styles, /color: blue/); }); it('clears the injection buffer', () => { startBuffering(); css(sheet.red); css(sheet.blue); flushToStyleTag(); let styleTags = global.document.getElementsByTagName("style"); assert.equal(styleTags.length, 1); const styleContentLength = getSheetText(styleTags[0].sheet).length; startBuffering(); flushToStyleTag(); styleTags = global.document.getElementsByTagName("style"); assert.equal(styleTags.length, 1); assert.equal(getSheetText(styleTags[0].sheet).length, styleContentLength); }); }); describe('flushToString', () => { it('returns the buffered styles', () => { startBuffering(); css(sheet.red); css(sheet.blue); const styles = flushToString(); assert.include(styles, `.${sheet.red._name}{`); assert.include(styles, `.${sheet.blue._name}{`); assert.match(styles, /color:red/); assert.match(styles, /color:blue/); }); it('clears the injection buffer', () => { startBuffering(); css(sheet.red); css(sheet.blue); assert.notEqual(flushToString(), ""); startBuffering(); assert.equal(flushToString(), ""); }); }); describe('getRenderedClassNames', () => { it('returns classes that have been rendered', () => { css(sheet.red); css(sheet.blue); const classNames = getRenderedClassNames(); assert.include(classNames, sheet.red._name); assert.include(classNames, sheet.blue._name); assert.notInclude(classNames, sheet.green._name); }); }); describe('addRenderedClassNames', () => { it('doesn\'t render classnames that were added', () => { startBuffering(); addRenderedClassNames([sheet.red._name, sheet.blue._name]); css(sheet.red); css(sheet.blue); css(sheet.green); flushToStyleTag(); const styleTags = global.document.getElementsByTagName("style"); assert.equal(styleTags.length, 1); const styles = getSheetText(styleTags[0].sheet); assert.include(styles, `.${sheet.green._name} {`); assert.notInclude(styles, `.${sheet.red._name} {`); assert.notInclude(styles, `.${sheet.blue._name} {`); assert.match(styles, /color: green/); assert.notMatch(styles, /color: red/); assert.notMatch(styles, /color: blue/); }); }); describe('resetInjectedStyle()', () => { it('injects styles again after being reset', () => { startBuffering(); css(sheet.red); flushToStyleTag(); const styleTags = global.document.getElementsByTagName("style"); const lastTag = styleTags[styleTags.length - 1]; assert.equal(getSheetText(lastTag.sheet), `.${sheet.red._name} {color: red !important;} `); // Delete all rules while (lastTag.sheet.cssRules.length > 0) { lastTag.sheet.deleteRule(0); } resetInjectedStyle(sheet.red._name); assert.equal(getSheetText(lastTag.sheet), ''); // Re-inject css(sheet.red); flushToStyleTag(); assert.equal(getSheetText(lastTag.sheet), `.${sheet.red._name} {color: red !important;} `); }); }); }); describe('String handlers', () => { beforeEach(() => { global.document = new JSDOM('').window.document; reset(); }); afterEach(() => { global.document.close(); global.document = undefined; }); function assertStylesInclude(str) { const styleTags = global.document.getElementsByTagName("style"); const styles = getSheetText(styleTags[0].sheet); assert.include(styles, str); } describe('fontFamily', () => { it('leaves plain strings alone', () => { const sheet = StyleSheet.create({ base: { fontFamily: "Helvetica", }, }); startBuffering(); css(sheet.base); flushToStyleTag(); assertStylesInclude('font-family: Helvetica !important'); }); it('concatenates arrays', () => { const sheet = StyleSheet.create({ base: { fontFamily: ["Helvetica", "sans-serif"], }, }); startBuffering(); css(sheet.base); flushToStyleTag(); assertStylesInclude('font-family: Helvetica,sans-serif !important'); }); it('adds @font-face rules for objects', () => { const fontface = { fontFamily: "CoolFont", src: "url('coolfont.ttf')", }; const sheet = StyleSheet.create({ base: { fontFamily: [fontface, "sans-serif"], }, }); startBuffering(); css(sheet.base); flushToStyleTag(); assertStylesInclude('font-family: "CoolFont",sans-serif !important'); assertStylesInclude('font-family: CoolFont;'); assertStylesInclude("src: url('coolfont.ttf');"); }); it('supports multiple @font-face with the same family name', () => { const sheet = StyleSheet.create({ base: { fontFamily: [ { fontFamily: "CoolFont", src: "url('coolfont.ttf')", }, { fontFamily: "CoolFont", fontStyle: "italic", src: "url('coolfont-italic.ttf')", }, { fontFamily: "CoolFont", fontWeight: 300, src: "url('coolfont-bold.ttf')", }, "sans-serif", ], }, }); startBuffering(); css(sheet.base); flushToStyleTag(); assertStylesInclude('font-family: "CoolFont",sans-serif !important'); assertStylesInclude('font-family: CoolFont;'); assertStylesInclude("src: url('coolfont.ttf');"); assertStylesInclude("font-style: italic; src: url('coolfont-italic.ttf');"); assertStylesInclude("font-weight: 300; src: url('coolfont-bold.ttf');"); }); it('supports multiple @font-face with different family names', () => { const sheet = StyleSheet.create({ base: { fontFamily: [ { fontFamily: "CoolFont", src: "url('coolfont.ttf')", }, { fontFamily: "AwesomeFont", src: "url('awesomefont.ttf')", }, { fontFamily: "SuperFont", src: "url('superfont.ttf')", }, "sans-serif", ], }, }); startBuffering(); css(sheet.base); flushToStyleTag(); assertStylesInclude('font-family: "CoolFont","AwesomeFont","SuperFont",sans-serif !important'); assertStylesInclude('font-family: CoolFont;'); assertStylesInclude("src: url('coolfont.ttf');"); assertStylesInclude('font-family: AwesomeFont;'); assertStylesInclude("src: url('awesomefont.ttf');"); assertStylesInclude('font-family: SuperFont;'); assertStylesInclude("src: url('superfont.ttf');"); }); }); describe('animationName', () => { it('leaves plain strings alone', () => { const sheet = StyleSheet.create({ animate: { animationName: "boo", }, }); startBuffering(); css(sheet.animate); flushToStyleTag(); assertStylesInclude('animation-name: boo !important'); }); it('generates css for keyframes', () => { const sheet = StyleSheet.create({ animate: { animationName: { 'from': { left: 10, }, '50%': { left: 20, }, 'to': { left: 40, }, }, }, }); startBuffering(); css(sheet.animate); flushToStyleTag(); assertStylesInclude('@keyframes keyframe_tmjr6'); assertStylesInclude('from {left: 10px;}'); assertStylesInclude('50% {left: 20px;}'); assertStylesInclude('to {left: 40px;}'); assertStylesInclude('animation-name: keyframe_tmjr6'); }); it('generates css for keyframes with multiple properties', () => { const sheet = StyleSheet.create({ animate: { animationName: { '0%': { opacity: 0, transform: 'scale(0.75) translate3d(1px, 2px, 0)', }, '100%': { opacity: 1, transform: 'scale(1) translate3d(1px, 2px, 0)', }, }, }, }); startBuffering(); css(sheet.animate); flushToStyleTag(); assertStylesInclude('@keyframes keyframe_d35t13'); assertStylesInclude('0% {opacity: 0; -webkit-transform: scale(0.75) translate3d(1px, 2px, 0); -ms-transform: scale(0.75) translate3d(1px, 2px, 0); transform: scale(0.75) translate3d(1px, 2px, 0);}'); assertStylesInclude('100% {opacity: 1; -webkit-transform: scale(1) translate3d(1px, 2px, 0); -ms-transform: scale(1) translate3d(1px, 2px, 0); transform: scale(1) translate3d(1px, 2px, 0);}'); assertStylesInclude('animation-name: keyframe_d35t13'); }); it('doesn\'t add the same keyframes twice', () => { const keyframes = { 'from': { left: 10, }, '50%': { left: 20, }, 'to': { left: 40, }, }; const sheet = StyleSheet.create({ animate: { animationName: keyframes, }, animate2: { animationName: keyframes, }, }); startBuffering(); css(sheet.animate); css(sheet.animate2); flushToStyleTag(); const styleTags = global.document.getElementsByTagName("style"); const styles = getSheetText(styleTags[0].sheet); assert.include(styles, '@keyframes keyframe_tmjr6'); assert.equal(styles.match(/@keyframes/g).length, 1); }); it('concatenates arrays of custom keyframes', () => { const keyframes1 = { 'from': { left: 10, }, 'to': { left: 50, }, }; const keyframes2 = { 'from': { top: -50, }, 'to': { top: 0, }, }; const sheet = StyleSheet.create({ animate: { animationName: [keyframes1, keyframes2], }, }); startBuffering(); css(sheet.animate); flushToStyleTag(); assertStylesInclude('@keyframes keyframe_1a8sduu'); assertStylesInclude('@keyframes keyframe_1wnshbu'); assertStylesInclude('animation-name: keyframe_1a8sduu,keyframe_1wnshbu') }); it('concatenates a custom keyframe animation with a plain string', () => { const keyframes1 = { 'from': { left: 10, }, 'to': { left: 50, }, }; const sheet = StyleSheet.create({ animate: { animationName: [keyframes1, 'hoo'], }, }); startBuffering(); css(sheet.animate); flushToStyleTag(); assertStylesInclude('@keyframes keyframe_1a8sduu'); assertStylesInclude('animation-name: keyframe_1a8sduu,hoo') }); }); });