react-email
Version:
A live preview of your emails right in your browser.
297 lines (268 loc) • 9.42 kB
text/typescript
import { generate, parse } from 'css-tree';
import { resolveAllCssVariables } from './resolve-all-css-variables.js';
describe('resolveAllCSSVariables', () => {
it('ignores @layer (properties) defined for browser compatibility', () => {
const root =
parse(`/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--color-red-500: oklch(63.7% 0.237 25.331);
--color-blue-400: oklch(70.7% 0.165 254.624);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-gray-200: oklch(92.8% 0.006 264.531);
--color-black: #000;
--color-white: #fff;
--spacing: 0.25rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--radius-md: 0.375rem;
}
}
@layer utilities {
.mt-8 {
margin-top: calc(var(--spacing) * 8);
}
.rounded-md {
border-radius: var(--radius-md);
}
.bg-blue-600 {
background-color: var(--color-blue-600);
}
.bg-red-500 {
background-color: var(--color-red-500);
}
.bg-white {
background-color: var(--color-white);
}
.p-4 {
padding: calc(var(--spacing) * 4);
}
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
.py-2 {
padding-block: calc(var(--spacing) * 2);
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.text-\\\\[14px\\\\] {
font-size: 14px;
}
.leading-\\\\[24px\\\\] {
--tw-leading: 24px;
line-height: 24px;
}
.text-black {
color: var(--color-black);
}
.text-blue-400 {
color: var(--color-blue-400);
}
.text-blue-600 {
color: var(--color-blue-600);
}
.text-gray-200 {
color: var(--color-gray-200);
}
.no-underline {
text-decoration-line: none;
}
}
@property --tw-leading {
syntax: "*";
inherits: false;
}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-leading: initial;
}
}
}
`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer properties;@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-400: oklch(70.7% 0.165 254.624);--color-blue-600: oklch(54.6% 0.245 262.881);--color-gray-200: oklch(92.8% 0.006 264.531);--color-black: #000;--color-white: #fff;--spacing: 0.25rem;--text-sm: 0.875rem;--text-sm--line-height: calc(1.25 / 0.875);--radius-md: 0.375rem}}@layer utilities{.mt-8{margin-top:calc(0.25rem*8)}.rounded-md{border-radius:0.375rem}.bg-blue-600{background-color:oklch(54.6%0.245 262.881)}.bg-red-500{background-color:oklch(63.7%0.237 25.331)}.bg-white{background-color:#fff}.p-4{padding:calc(0.25rem*4)}.px-3{padding-inline:calc(0.25rem*3)}.py-2{padding-block:calc(0.25rem*2)}.text-sm{font-size:0.875rem;line-height:calc(1.25/0.875)}.text-\\\\[14px\\\\]{font-size:14px}.leading-\\\\[24px\\\\]{--tw-leading: 24px;line-height:24px}.text-black{color:#000}.text-blue-400{color:oklch(70.7%0.165 254.624)}.text-blue-600{color:oklch(54.6%0.245 262.881)}.text-gray-200{color:oklch(92.8%0.006 264.531)}.no-underline{text-decoration-line:none}}@property --tw-leading{syntax:"*";inherits:false}@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::before,::after,::backdrop{--tw-leading: initial}}}"`,
);
});
it('works with simple css variables on a :root', () => {
const root = parse(`:root {
--width: 100px;
}
.box {
width: var(--width);
}`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`":root{--width: 100px}.box{width:100px}"`,
);
});
it('works for variables across different CSS layers', () => {
const root = parse(`@layer base {
:root {
--width: 100px;
}
}
@layer utilities {
.box {
width: var(--width);
}
}`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`"@layer base{:root{--width: 100px}}@layer utilities{.box{width:100px}}"`,
);
});
it('works with multiple variables in the same declaration', () => {
const root = parse(`:root {
--top: 101px;
--bottom: 102px;
--right: 103px;
--left: 104px;
}
.box {
margin: var(--top) var(--right) var(--bottom) var(--left);
}`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`":root{--top: 101px;--bottom: 102px;--right: 103px;--left: 104px}.box{margin:101px 103px 102px 104px}"`,
);
});
it('keeps variable usages if it cant find their declaration', () => {
const root = parse(`.box {
width: var(--width);
}`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(`".box{width:var(--width)}"`);
});
it('works with variables set in the same rule', () => {
const root = parse(`.box {
--width: 200px;
width: var(--width);
}
@media (min-width: 1280px) {
.xl\\\\:bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity))
}
}
`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`".box{--width: 200px;width:200px}@media (min-width:1280px){.xl\\\\:bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94/1)}}"`,
);
});
it('works with a variable set in a layer, and used in another through a media query', () => {
const root = parse(`@layer theme {
:root {
--color-blue-300: blue;
}
}
@layer utilities {
.sm\\\\:bg-blue-300 {
@media (width >= 40rem) {
background-color: var(--color-blue-300);
}
}
}`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`"@layer theme{:root{--color-blue-300: blue}}@layer utilities{.sm\\\\:bg-blue-300{@media (width>=40rem){background-color:blue}}}"`,
);
});
it('uses fallback values when variable definition is not found', () => {
const root = parse(`.box {
width: var(--undefined-width, 150px);
height: var(--undefined-height, 200px);
margin: var(--undefined-margin, 10px 20px);
}`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`".box{width:150px;height:200px;margin:10px 20px}"`,
);
});
it('handles nested var() functions in fallbacks', () => {
const root = parse(`:root {
--fallback-width: 300px;
}
.box {
width: var(--undefined-width, var(--fallback-width));
height: var(--undefined-height, var(--also-undefined, 250px));
}`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`":root{--fallback-width: 300px}.box{width:300px;height:250px}"`,
);
});
it('handles deeply nested var() functions with complex parentheses', () => {
const root = parse(`:root {
--primary: blue;
--secondary: red;
--fallback: green;
--size: 20px;
}
.box {
color: var(--primary, var(--secondary, var(--fallback)));
width: var(--size, calc(100px + var(--size, 20px)));
border: var(--border-width, var(--border-style, var(--border-color, 1px solid black)));
--r: 100;
--b: 10;
background: var(--bg-color, rgb(var(--r, 255), var(--g, 0), var(--b, 0)));
}`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`":root{--primary: blue;--secondary: red;--fallback: green;--size: 20px}.box{color:blue;width:20px;border:1px solid black;--r: 100;--b: 10;background:rgb(100,0,10)}"`,
);
});
it('does not leak variables across sibling nested at-rules within the same rule', () => {
const root = parse(`.print_invert {
@media print {
--tw-invert: invert(100%);
filter: var(--tw-invert, none);
}
@media screen {
filter: var(--tw-invert, none);
}
}`);
resolveAllCssVariables(root);
expect(generate(root)).toMatchInlineSnapshot(
`".print_invert{@media print{--tw-invert: invert(100%);filter:invert(100%)}@media screen{filter:none}}"`,
);
});
it('handles selectors with asterisks in attribute selectors and pseudo-functions', () => {
const root = parse(`* {
--global-color: red;
}
input[type="*"]:hover {
color: var(--global-color);
}
div:nth-child(2*n+1) {
background: var(--global-color);
}
.test[data-attr="value*test"] {
border-color: var(--global-color);
}
.universal-with-class-* {
--class-color: blue;
text-decoration: var(--class-color);
}
.normal {
color: var(--class-color);
}`);
resolveAllCssVariables(root);
const result = generate(root);
// Variables from universal selector (*) should resolve to other selectors with actual universal selector
expect(result).toContain('input[type="*"]:hover{color:red}');
expect(result).toContain('div:nth-child(2*n+1){background:red}');
expect(result).toContain('.test[data-attr="value*test"]{border-color:red}');
// Variables from *.universal-with-class should resolve within the same selector and to .normal
expect(result).toContain(
'.universal-with-class-*{--class-color: blue;text-decoration:blue}',
);
expect(result).toContain('.normal{color:var(--class-color)}');
});
});