1 | import asap from 'asap';
|
2 | import {assert} from 'chai';
|
3 | import {JSDOM} from 'jsdom';
|
4 |
|
5 | import { StyleSheet, css, minify } from '../src/index';
|
6 | import {
|
7 | injectAndGetClassName,
|
8 | injectStyleOnce,
|
9 | reset,
|
10 | resetInjectedStyle,
|
11 | startBuffering,
|
12 | flushToString,
|
13 | flushToStyleTag,
|
14 | addRenderedClassNames,
|
15 | getRenderedClassNames,
|
16 | } from '../src/inject';
|
17 | import { defaultSelectorHandlers } from '../src/generate';
|
18 | import { getSheetText } from './testUtils';
|
19 |
|
20 | const sheet = StyleSheet.create({
|
21 | red: {
|
22 | color: 'red',
|
23 | },
|
24 |
|
25 | blue: {
|
26 | color: 'blue',
|
27 | },
|
28 |
|
29 | green: {
|
30 | color: 'green',
|
31 | },
|
32 | });
|
33 |
|
34 | describe('injection', () => {
|
35 | beforeEach(() => {
|
36 | global.document = new JSDOM('').window.document;
|
37 | reset();
|
38 | });
|
39 |
|
40 | afterEach(() => {
|
41 | global.document.close();
|
42 | global.document = undefined;
|
43 | });
|
44 |
|
45 | describe('injectAndGetClassName', () => {
|
46 | it('uses hashed class name', () => {
|
47 | const className = injectAndGetClassName(false, [sheet.red], defaultSelectorHandlers);
|
48 | assert.equal(className, 'red_137u7ef');
|
49 | });
|
50 |
|
51 | it('combines class names', () => {
|
52 | const className = injectAndGetClassName(false, [sheet.red, sheet.blue, sheet.green], defaultSelectorHandlers);
|
53 | assert.equal(className, 'red_137u7ef-o_O-blue_1tsdo2i-o_O-green_1jzdmtb');
|
54 | });
|
55 |
|
56 | it('ignores null values in styleDefinitions', () => {
|
57 | const className = injectAndGetClassName(false, [sheet.red, sheet.blue, null], defaultSelectorHandlers);
|
58 | assert.equal(className, 'red_137u7ef-o_O-blue_1tsdo2i');
|
59 | });
|
60 |
|
61 | describe('process.env.NODE_ENV === \'production\'', () => {
|
62 | let prodSheet;
|
63 | beforeEach(() => {
|
64 | process.env.NODE_ENV = 'production';
|
65 | minify(true);
|
66 | prodSheet = StyleSheet.create({
|
67 | red: {
|
68 | color: 'red',
|
69 | },
|
70 |
|
71 | blue: {
|
72 | color: 'blue',
|
73 | },
|
74 |
|
75 | green: {
|
76 | color: 'green',
|
77 | },
|
78 | });
|
79 | });
|
80 |
|
81 | afterEach(() => {
|
82 | delete process.env.NODE_ENV;
|
83 | minify(false);
|
84 | });
|
85 |
|
86 | it('uses hashed class name (does not re-hash)', () => {
|
87 | const className = injectAndGetClassName(false, [prodSheet.red], defaultSelectorHandlers);
|
88 | assert.equal(className, `_${prodSheet.red._name}`);
|
89 | });
|
90 |
|
91 | it('creates minified combined class name', () => {
|
92 | const className = injectAndGetClassName(false, [prodSheet.red, prodSheet.blue, prodSheet.green], defaultSelectorHandlers);
|
93 | assert.equal(className, '_11v1eztc');
|
94 | });
|
95 |
|
96 | it('ignores null values in styleDefinitions', () => {
|
97 | const className = injectAndGetClassName(false, [
|
98 | null,
|
99 | prodSheet.red,
|
100 | null,
|
101 | prodSheet.blue,
|
102 | prodSheet.green,
|
103 | null
|
104 | ], defaultSelectorHandlers);
|
105 | assert.equal(className, '_11v1eztc');
|
106 | });
|
107 | });
|
108 | });
|
109 |
|
110 | describe('injectStyleOnce', () => {
|
111 | it('causes styles to automatically be added', done => {
|
112 | injectStyleOnce("x", ".x", [{ color: "red" }], false);
|
113 |
|
114 | asap(() => {
|
115 | const styleTags = global.document.getElementsByTagName("style");
|
116 | assert.equal(styleTags.length, 1);
|
117 | const styles = getSheetText(styleTags[0].sheet);
|
118 |
|
119 | assert.include(styles, ".x {");
|
120 | assert.include(styles, "color: red");
|
121 |
|
122 | done();
|
123 | });
|
124 | });
|
125 |
|
126 | it('causes styles to be added async, and buffered', done => {
|
127 | injectStyleOnce("x", ".x", [{ color: "red" }], false);
|
128 |
|
129 | const styleTags = global.document.getElementsByTagName("style");
|
130 | assert.equal(styleTags.length, 0);
|
131 |
|
132 | injectStyleOnce("y", ".y", [{ color: "blue" }], false);
|
133 |
|
134 | asap(() => {
|
135 | const styleTags = global.document.getElementsByTagName("style");
|
136 | assert.equal(styleTags.length, 1);
|
137 | const styles = getSheetText(styleTags[0].sheet);
|
138 |
|
139 | assert.include(styles, ".x {");
|
140 | assert.include(styles, ".y {");
|
141 | assert.include(styles, "color: red");
|
142 | assert.include(styles, "color: blue");
|
143 |
|
144 | done();
|
145 | });
|
146 | });
|
147 |
|
148 | it('doesn\'t inject the same style twice', done => {
|
149 | injectStyleOnce("x", ".x", [{ color: "red" }], false);
|
150 | injectStyleOnce("x", ".x", [{ color: "blue" }], false);
|
151 |
|
152 | asap(() => {
|
153 | const styleTags = global.document.getElementsByTagName("style");
|
154 | assert.equal(styleTags.length, 1);
|
155 | const styles = getSheetText(styleTags[0].sheet);
|
156 |
|
157 | assert.include(styles, ".x {");
|
158 | assert.include(styles, "color: red");
|
159 | assert.notInclude(styles, "color: blue");
|
160 | assert.equal(styles.match(/\.x {/g).length, 1);
|
161 |
|
162 | done();
|
163 | });
|
164 | });
|
165 |
|
166 | it('throws an error if we\'re not buffering and on the server', () => {
|
167 | const oldDocument = global.document;
|
168 | global.document = undefined;
|
169 |
|
170 | assert.throws(() => {
|
171 | injectStyleOnce("x", ".x", [{ color: "red" }], false);
|
172 | }, "Cannot automatically buffer");
|
173 |
|
174 | global.document = oldDocument;
|
175 | });
|
176 |
|
177 |
|
178 | it('adds to .innerText if insertRule is not available', done => {
|
179 | const styleTag = global.document.createElement("style");
|
180 | styleTag.setAttribute("data-aphrodite", "");
|
181 | document.head.appendChild(styleTag);
|
182 | styleTag.sheet.insertRule = null;
|
183 |
|
184 | injectStyleOnce("x", ".x", [{ color: "red" }], false);
|
185 |
|
186 | asap(() => {
|
187 | assert.include(styleTag.innerText, ".x{");
|
188 | assert.include(styleTag.innerText, "color:red");
|
189 | done();
|
190 | });
|
191 | });
|
192 |
|
193 | it('uses document.getElementsByTagName without document.head', done => {
|
194 | Object.defineProperty(global.document, "head", {
|
195 | value: null,
|
196 | });
|
197 |
|
198 | injectStyleOnce("x", ".x", [{ color: "red" }], false);
|
199 |
|
200 | asap(() => {
|
201 | const styleTags = global.document.getElementsByTagName("style");
|
202 | assert.equal(styleTags.length, 1);
|
203 | const styles = getSheetText(styleTags[0].sheet);
|
204 |
|
205 | assert.include(styles, ".x {");
|
206 | assert.include(styles, "color: red");
|
207 |
|
208 | done();
|
209 | });
|
210 | });
|
211 | });
|
212 |
|
213 | describe('startBuffering', () => {
|
214 | it('causes styles to not be added automatically', done => {
|
215 | startBuffering();
|
216 |
|
217 | css(sheet.red);
|
218 |
|
219 | asap(() => {
|
220 | const styleTags = global.document.getElementsByTagName("style");
|
221 | assert.equal(styleTags.length, 0);
|
222 | done();
|
223 | });
|
224 | });
|
225 |
|
226 | it('throws an error if we try to buffer twice', () => {
|
227 | startBuffering();
|
228 |
|
229 | assert.throws(() => {
|
230 | startBuffering();
|
231 | }, "already buffering");
|
232 | });
|
233 | });
|
234 |
|
235 | describe('flushToStyleTag', () => {
|
236 | it('adds a style tag with all the buffered styles', () => {
|
237 | startBuffering();
|
238 |
|
239 | css(sheet.red);
|
240 | css(sheet.blue);
|
241 |
|
242 | flushToStyleTag();
|
243 |
|
244 | const styleTags = global.document.getElementsByTagName("style");
|
245 | const lastTag = styleTags[styleTags.length - 1];
|
246 | const styles = getSheetText(lastTag.sheet);
|
247 |
|
248 | assert.include(styles, `.${sheet.red._name} {`);
|
249 | assert.include(styles, `.${sheet.blue._name} {`);
|
250 | assert.match(styles, /color: red/);
|
251 | assert.match(styles, /color: blue/);
|
252 | });
|
253 |
|
254 | it('clears the injection buffer', () => {
|
255 | startBuffering();
|
256 |
|
257 | css(sheet.red);
|
258 | css(sheet.blue);
|
259 |
|
260 | flushToStyleTag();
|
261 |
|
262 | let styleTags = global.document.getElementsByTagName("style");
|
263 | assert.equal(styleTags.length, 1);
|
264 | const styleContentLength = getSheetText(styleTags[0].sheet).length;
|
265 |
|
266 | startBuffering();
|
267 | flushToStyleTag();
|
268 |
|
269 | styleTags = global.document.getElementsByTagName("style");
|
270 | assert.equal(styleTags.length, 1);
|
271 | assert.equal(getSheetText(styleTags[0].sheet).length, styleContentLength);
|
272 | });
|
273 | });
|
274 |
|
275 | describe('flushToString', () => {
|
276 | it('returns the buffered styles', () => {
|
277 | startBuffering();
|
278 |
|
279 | css(sheet.red);
|
280 | css(sheet.blue);
|
281 |
|
282 | const styles = flushToString();
|
283 |
|
284 | assert.include(styles, `.${sheet.red._name}{`);
|
285 | assert.include(styles, `.${sheet.blue._name}{`);
|
286 | assert.match(styles, /color:red/);
|
287 | assert.match(styles, /color:blue/);
|
288 | });
|
289 |
|
290 | it('clears the injection buffer', () => {
|
291 | startBuffering();
|
292 |
|
293 | css(sheet.red);
|
294 | css(sheet.blue);
|
295 |
|
296 | assert.notEqual(flushToString(), "");
|
297 |
|
298 | startBuffering();
|
299 | assert.equal(flushToString(), "");
|
300 | });
|
301 | });
|
302 |
|
303 | describe('getRenderedClassNames', () => {
|
304 | it('returns classes that have been rendered', () => {
|
305 | css(sheet.red);
|
306 | css(sheet.blue);
|
307 |
|
308 | const classNames = getRenderedClassNames();
|
309 |
|
310 | assert.include(classNames, sheet.red._name);
|
311 | assert.include(classNames, sheet.blue._name);
|
312 | assert.notInclude(classNames, sheet.green._name);
|
313 | });
|
314 | });
|
315 |
|
316 | describe('addRenderedClassNames', () => {
|
317 | it('doesn\'t render classnames that were added', () => {
|
318 | startBuffering();
|
319 | addRenderedClassNames([sheet.red._name, sheet.blue._name]);
|
320 |
|
321 | css(sheet.red);
|
322 | css(sheet.blue);
|
323 | css(sheet.green);
|
324 |
|
325 | flushToStyleTag();
|
326 |
|
327 | const styleTags = global.document.getElementsByTagName("style");
|
328 | assert.equal(styleTags.length, 1);
|
329 | const styles = getSheetText(styleTags[0].sheet);
|
330 |
|
331 | assert.include(styles, `.${sheet.green._name} {`);
|
332 | assert.notInclude(styles, `.${sheet.red._name} {`);
|
333 | assert.notInclude(styles, `.${sheet.blue._name} {`);
|
334 | assert.match(styles, /color: green/);
|
335 | assert.notMatch(styles, /color: red/);
|
336 | assert.notMatch(styles, /color: blue/);
|
337 | });
|
338 | });
|
339 |
|
340 | describe('resetInjectedStyle()', () => {
|
341 | it('injects styles again after being reset', () => {
|
342 | startBuffering();
|
343 |
|
344 | css(sheet.red);
|
345 |
|
346 | flushToStyleTag();
|
347 |
|
348 | const styleTags = global.document.getElementsByTagName("style");
|
349 | const lastTag = styleTags[styleTags.length - 1];
|
350 |
|
351 | assert.equal(getSheetText(lastTag.sheet), `.${sheet.red._name} {color: red !important;} `);
|
352 |
|
353 |
|
354 | while (lastTag.sheet.cssRules.length > 0) {
|
355 | lastTag.sheet.deleteRule(0);
|
356 | }
|
357 |
|
358 | resetInjectedStyle(sheet.red._name);
|
359 |
|
360 | assert.equal(getSheetText(lastTag.sheet), '');
|
361 |
|
362 |
|
363 | css(sheet.red);
|
364 |
|
365 | flushToStyleTag();
|
366 |
|
367 | assert.equal(getSheetText(lastTag.sheet), `.${sheet.red._name} {color: red !important;} `);
|
368 | });
|
369 | });
|
370 | });
|
371 |
|
372 | describe('String handlers', () => {
|
373 | beforeEach(() => {
|
374 | global.document = new JSDOM('').window.document;
|
375 | reset();
|
376 | });
|
377 |
|
378 | afterEach(() => {
|
379 | global.document.close();
|
380 | global.document = undefined;
|
381 | });
|
382 |
|
383 | function assertStylesInclude(str) {
|
384 | const styleTags = global.document.getElementsByTagName("style");
|
385 | const styles = getSheetText(styleTags[0].sheet);
|
386 |
|
387 | assert.include(styles, str);
|
388 | }
|
389 |
|
390 | describe('fontFamily', () => {
|
391 | it('leaves plain strings alone', () => {
|
392 | const sheet = StyleSheet.create({
|
393 | base: {
|
394 | fontFamily: "Helvetica",
|
395 | },
|
396 | });
|
397 |
|
398 | startBuffering();
|
399 | css(sheet.base);
|
400 | flushToStyleTag();
|
401 |
|
402 | assertStylesInclude('font-family: Helvetica !important');
|
403 | });
|
404 |
|
405 | it('concatenates arrays', () => {
|
406 | const sheet = StyleSheet.create({
|
407 | base: {
|
408 | fontFamily: ["Helvetica", "sans-serif"],
|
409 | },
|
410 | });
|
411 |
|
412 | startBuffering();
|
413 | css(sheet.base);
|
414 | flushToStyleTag();
|
415 |
|
416 | assertStylesInclude('font-family: Helvetica,sans-serif !important');
|
417 | });
|
418 |
|
419 | it('adds @font-face rules for objects', () => {
|
420 | const fontface = {
|
421 | fontFamily: "CoolFont",
|
422 | src: "url('coolfont.ttf')",
|
423 | };
|
424 |
|
425 | const sheet = StyleSheet.create({
|
426 | base: {
|
427 | fontFamily: [fontface, "sans-serif"],
|
428 | },
|
429 | });
|
430 |
|
431 | startBuffering();
|
432 | css(sheet.base);
|
433 | flushToStyleTag();
|
434 |
|
435 | assertStylesInclude('font-family: "CoolFont",sans-serif !important');
|
436 | assertStylesInclude('font-family: CoolFont;');
|
437 | assertStylesInclude("src: url('coolfont.ttf');");
|
438 | });
|
439 |
|
440 | it('supports multiple @font-face with the same family name', () => {
|
441 | const sheet = StyleSheet.create({
|
442 | base: {
|
443 | fontFamily: [
|
444 | {
|
445 | fontFamily: "CoolFont",
|
446 | src: "url('coolfont.ttf')",
|
447 | },
|
448 | {
|
449 | fontFamily: "CoolFont",
|
450 | fontStyle: "italic",
|
451 | src: "url('coolfont-italic.ttf')",
|
452 | },
|
453 | {
|
454 | fontFamily: "CoolFont",
|
455 | fontWeight: 300,
|
456 | src: "url('coolfont-bold.ttf')",
|
457 | },
|
458 | "sans-serif",
|
459 | ],
|
460 | },
|
461 | });
|
462 |
|
463 | startBuffering();
|
464 | css(sheet.base);
|
465 | flushToStyleTag();
|
466 |
|
467 | assertStylesInclude('font-family: "CoolFont",sans-serif !important');
|
468 | assertStylesInclude('font-family: CoolFont;');
|
469 | assertStylesInclude("src: url('coolfont.ttf');");
|
470 | assertStylesInclude("font-style: italic; src: url('coolfont-italic.ttf');");
|
471 | assertStylesInclude("font-weight: 300; src: url('coolfont-bold.ttf');");
|
472 | });
|
473 |
|
474 | it('supports multiple @font-face with different family names', () => {
|
475 | const sheet = StyleSheet.create({
|
476 | base: {
|
477 | fontFamily: [
|
478 | {
|
479 | fontFamily: "CoolFont",
|
480 | src: "url('coolfont.ttf')",
|
481 | },
|
482 | {
|
483 | fontFamily: "AwesomeFont",
|
484 | src: "url('awesomefont.ttf')",
|
485 | },
|
486 | {
|
487 | fontFamily: "SuperFont",
|
488 | src: "url('superfont.ttf')",
|
489 | },
|
490 | "sans-serif",
|
491 | ],
|
492 | },
|
493 | });
|
494 |
|
495 | startBuffering();
|
496 | css(sheet.base);
|
497 | flushToStyleTag();
|
498 |
|
499 | assertStylesInclude('font-family: "CoolFont","AwesomeFont","SuperFont",sans-serif !important');
|
500 | assertStylesInclude('font-family: CoolFont;');
|
501 | assertStylesInclude("src: url('coolfont.ttf');");
|
502 | assertStylesInclude('font-family: AwesomeFont;');
|
503 | assertStylesInclude("src: url('awesomefont.ttf');");
|
504 | assertStylesInclude('font-family: SuperFont;');
|
505 | assertStylesInclude("src: url('superfont.ttf');");
|
506 | });
|
507 | });
|
508 |
|
509 | describe('animationName', () => {
|
510 | it('leaves plain strings alone', () => {
|
511 | const sheet = StyleSheet.create({
|
512 | animate: {
|
513 | animationName: "boo",
|
514 | },
|
515 | });
|
516 |
|
517 | startBuffering();
|
518 | css(sheet.animate);
|
519 | flushToStyleTag();
|
520 |
|
521 | assertStylesInclude('animation-name: boo !important');
|
522 | });
|
523 |
|
524 | it('generates css for keyframes', () => {
|
525 | const sheet = StyleSheet.create({
|
526 | animate: {
|
527 | animationName: {
|
528 | 'from': {
|
529 | left: 10,
|
530 | },
|
531 | '50%': {
|
532 | left: 20,
|
533 | },
|
534 | 'to': {
|
535 | left: 40,
|
536 | },
|
537 | },
|
538 | },
|
539 | });
|
540 |
|
541 | startBuffering();
|
542 | css(sheet.animate);
|
543 | flushToStyleTag();
|
544 |
|
545 | assertStylesInclude('@keyframes keyframe_tmjr6');
|
546 | assertStylesInclude('from {left: 10px;}');
|
547 | assertStylesInclude('50% {left: 20px;}');
|
548 | assertStylesInclude('to {left: 40px;}');
|
549 | assertStylesInclude('animation-name: keyframe_tmjr6');
|
550 | });
|
551 |
|
552 | it('generates css for keyframes with multiple properties', () => {
|
553 | const sheet = StyleSheet.create({
|
554 | animate: {
|
555 | animationName: {
|
556 | '0%': {
|
557 | opacity: 0,
|
558 | transform: 'scale(0.75) translate3d(1px, 2px, 0)',
|
559 | },
|
560 | '100%': {
|
561 | opacity: 1,
|
562 | transform: 'scale(1) translate3d(1px, 2px, 0)',
|
563 | },
|
564 | },
|
565 | },
|
566 | });
|
567 |
|
568 | startBuffering();
|
569 | css(sheet.animate);
|
570 | flushToStyleTag();
|
571 |
|
572 | assertStylesInclude('@keyframes keyframe_d35t13');
|
573 | 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);}');
|
574 | 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);}');
|
575 | assertStylesInclude('animation-name: keyframe_d35t13');
|
576 | });
|
577 |
|
578 | it('doesn\'t add the same keyframes twice', () => {
|
579 | const keyframes = {
|
580 | 'from': {
|
581 | left: 10,
|
582 | },
|
583 | '50%': {
|
584 | left: 20,
|
585 | },
|
586 | 'to': {
|
587 | left: 40,
|
588 | },
|
589 | };
|
590 |
|
591 | const sheet = StyleSheet.create({
|
592 | animate: {
|
593 | animationName: keyframes,
|
594 | },
|
595 | animate2: {
|
596 | animationName: keyframes,
|
597 | },
|
598 | });
|
599 |
|
600 | startBuffering();
|
601 | css(sheet.animate);
|
602 | css(sheet.animate2);
|
603 | flushToStyleTag();
|
604 |
|
605 | const styleTags = global.document.getElementsByTagName("style");
|
606 | const styles = getSheetText(styleTags[0].sheet);
|
607 |
|
608 | assert.include(styles, '@keyframes keyframe_tmjr6');
|
609 | assert.equal(styles.match(/@keyframes/g).length, 1);
|
610 | });
|
611 |
|
612 | it('concatenates arrays of custom keyframes', () => {
|
613 | const keyframes1 = {
|
614 | 'from': {
|
615 | left: 10,
|
616 | },
|
617 | 'to': {
|
618 | left: 50,
|
619 | },
|
620 | };
|
621 |
|
622 | const keyframes2 = {
|
623 | 'from': {
|
624 | top: -50,
|
625 | },
|
626 | 'to': {
|
627 | top: 0,
|
628 | },
|
629 | };
|
630 |
|
631 | const sheet = StyleSheet.create({
|
632 | animate: {
|
633 | animationName: [keyframes1, keyframes2],
|
634 | },
|
635 | });
|
636 |
|
637 | startBuffering();
|
638 | css(sheet.animate);
|
639 | flushToStyleTag();
|
640 |
|
641 | assertStylesInclude('@keyframes keyframe_1a8sduu');
|
642 | assertStylesInclude('@keyframes keyframe_1wnshbu');
|
643 | assertStylesInclude('animation-name: keyframe_1a8sduu,keyframe_1wnshbu')
|
644 | });
|
645 |
|
646 | it('concatenates a custom keyframe animation with a plain string', () => {
|
647 | const keyframes1 = {
|
648 | 'from': {
|
649 | left: 10,
|
650 | },
|
651 | 'to': {
|
652 | left: 50,
|
653 | },
|
654 | };
|
655 |
|
656 | const sheet = StyleSheet.create({
|
657 | animate: {
|
658 | animationName: [keyframes1, 'hoo'],
|
659 | },
|
660 | });
|
661 |
|
662 | startBuffering();
|
663 | css(sheet.animate);
|
664 | flushToStyleTag();
|
665 |
|
666 | assertStylesInclude('@keyframes keyframe_1a8sduu');
|
667 | assertStylesInclude('animation-name: keyframe_1a8sduu,hoo')
|
668 | });
|
669 | });
|
670 | });
|