import _ from "lodash" import postcss from "postcss" import processPlugins from "../src/util/processPlugins" function css(nodes) { return postcss.root({ nodes }).toString() } function config(overrides) { return _.defaultsDeep(overrides, { options: { prefix: "", important: false, separator: ":" } }) } test("plugins can create utilities with object syntax", () => { const { components, utilities } = processPlugins( [ function({ addUtilities }) { addUtilities({ ".object-fill": { "object-fit": "fill" }, ".object-contain": { "object-fit": "contain" }, ".object-cover": { "object-fit": "cover" } }) } ], config() ) expect(components.length).toBe(0) expect(css(utilities)).toMatchCss(` @variants { .object-fill { object-fit: fill } .object-contain { object-fit: contain } .object-cover { object-fit: cover } } `) }) test("plugins can create utilities with arrays of objects", () => { const { components, utilities } = processPlugins( [ function({ addUtilities }) { addUtilities([ { ".object-fill": { "object-fit": "fill" } }, { ".object-contain": { "object-fit": "contain" } }, { ".object-cover": { "object-fit": "cover" } } ]) } ], config() ) expect(components.length).toBe(0) expect(css(utilities)).toMatchCss(` @variants { .object-fill { object-fit: fill } .object-contain { object-fit: contain } .object-cover { object-fit: cover } } `) }) test("plugins can create utilities with raw PostCSS nodes", () => { const { components, utilities } = processPlugins( [ function({ addUtilities }) { addUtilities([ postcss.rule({ selector: ".object-fill" }).append([ postcss.decl({ prop: "object-fit", value: "fill" }) ]), postcss.rule({ selector: ".object-contain" }).append([ postcss.decl({ prop: "object-fit", value: "contain" }) ]), postcss.rule({ selector: ".object-cover" }).append([ postcss.decl({ prop: "object-fit", value: "cover" }) ]) ]) } ], config() ) expect(components.length).toBe(0) expect(css(utilities)).toMatchCss(` @variants { .object-fill { object-fit: fill } .object-contain { object-fit: contain } .object-cover { object-fit: cover } } `) }) test("plugins can create utilities with mixed object styles and PostCSS nodes", () => { const { components, utilities } = processPlugins( [ function({ addUtilities }) { addUtilities([ { ".object-fill": { objectFit: "fill" } }, postcss.rule({ selector: ".object-contain" }).append([ postcss.decl({ prop: "object-fit", value: "contain" }) ]), postcss.rule({ selector: ".object-cover" }).append([ postcss.decl({ prop: "object-fit", value: "cover" }) ]) ]) } ], config() ) expect(components.length).toBe(0) expect(css(utilities)).toMatchCss(` @variants { .object-fill { object-fit: fill } .object-contain { object-fit: contain } .object-cover { object-fit: cover } } `) }) test("plugins can create utilities with variants", () => { const { components, utilities } = processPlugins( [ function({ addUtilities }) { addUtilities( { ".object-fill": { "object-fit": "fill" }, ".object-contain": { "object-fit": "contain" }, ".object-cover": { "object-fit": "cover" } }, ["responsive", "hover", "group-hover", "focus"] ) } ], config() ) expect(components.length).toBe(0) expect(css(utilities)).toMatchCss(` @variants responsive, hover, group-hover, focus { .object-fill { object-fit: fill } .object-contain { object-fit: contain } .object-cover { object-fit: cover } } `) }) test("plugins can create components with object syntax", () => { const { components, utilities } = processPlugins( [ function({ addComponents }) { addComponents({ ".btn-blue": { backgroundColor: "blue", color: "white", padding: ".5rem 1rem", borderRadius: ".25rem" }, ".btn-blue:hover": { backgroundColor: "darkblue" } }) } ], config() ) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` .btn-blue { background-color: blue; color: white; padding: .5rem 1rem; border-radius: .25rem } .btn-blue:hover { background-color: darkblue } `) }) test("plugins can create components with raw PostCSS nodes", () => { const { components, utilities } = processPlugins( [ function({ addComponents }) { addComponents([ postcss.rule({ selector: ".btn-blue" }).append([ postcss.decl({ prop: "background-color", value: "blue" }), postcss.decl({ prop: "color", value: "white" }), postcss.decl({ prop: "padding", value: ".5rem 1rem" }), postcss.decl({ prop: "border-radius", value: ".25rem" }) ]), postcss.rule({ selector: ".btn-blue:hover" }).append([ postcss.decl({ prop: "background-color", value: "darkblue" }) ]) ]) } ], config() ) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` .btn-blue { background-color: blue; color: white; padding: .5rem 1rem; border-radius: .25rem } .btn-blue:hover { background-color: darkblue } `) }) test("plugins can create components with mixed object styles and raw PostCSS nodes", () => { const { components, utilities } = processPlugins( [ function({ addComponents }) { addComponents([ postcss.rule({ selector: ".btn-blue" }).append([ postcss.decl({ prop: "background-color", value: "blue" }), postcss.decl({ prop: "color", value: "white" }), postcss.decl({ prop: "padding", value: ".5rem 1rem" }), postcss.decl({ prop: "border-radius", value: ".25rem" }) ]), { ".btn-blue:hover": { backgroundColor: "darkblue" } } ]) } ], config() ) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` .btn-blue { background-color: blue; color: white; padding: .5rem 1rem; border-radius: .25rem } .btn-blue:hover { background-color: darkblue } `) }) test("plugins can create components with media queries with object syntax", () => { const { components, utilities } = processPlugins( [ function({ addComponents }) { addComponents({ ".container": { width: "100%" }, "@media (min-width: 100px)": { ".container": { maxWidth: "100px" } }, "@media (min-width: 200px)": { ".container": { maxWidth: "200px" } }, "@media (min-width: 300px)": { ".container": { maxWidth: "300px" } } }) } ], config() ) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` .container { width: 100% } @media (min-width: 100px) { .container { max-width: 100px } } @media (min-width: 200px) { .container { max-width: 200px } } @media (min-width: 300px) { .container { max-width: 300px } } `) }) test("media queries can be defined multiple times using objects-in-array syntax", () => { const { components, utilities } = processPlugins( [ function({ addComponents }) { addComponents([ { ".container": { width: "100%" }, "@media (min-width: 100px)": { ".container": { maxWidth: "100px" } } }, { ".btn": { padding: "1rem .5rem", display: "block" }, "@media (min-width: 100px)": { ".btn": { display: "inline-block" } } } ]) } ], config() ) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` .container { width: 100% } @media (min-width: 100px) { .container { max-width: 100px } } .btn { padding: 1rem .5rem; display: block } @media (min-width: 100px) { .btn { display: inline-block } } `) }) test("plugins can create nested rules", () => { const { components, utilities } = processPlugins( [ function({ addComponents }) { addComponents({ ".btn-blue": { backgroundColor: "blue", color: "white", padding: ".5rem 1rem", borderRadius: ".25rem", "&:hover": { backgroundColor: "darkblue" }, "@media (min-width: 500px)": { "&:hover": { backgroundColor: "orange" } }, "> a": { color: "red" }, "h1 &": { color: "purple" } } }) } ], config() ) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` .btn-blue { background-color: blue; color: white; padding: .5rem 1rem; border-radius: .25rem; } .btn-blue:hover { background-color: darkblue; } @media (min-width: 500px) { .btn-blue:hover { background-color: orange; } } .btn-blue > a { color: red; } h1 .btn-blue { color: purple; } `) }) test("plugins can create rules with escaped selectors", () => { const { components, utilities } = processPlugins( [ function({ e, addUtilities }) { addUtilities({ [`.${e("top-1/4")}`]: { top: "25%" } }) } ], config() ) expect(components.length).toBe(0) expect(css(utilities)).toMatchCss(` @variants { .top-1\\/4 { top: 25% } } `) }) test("plugins can access the current config", () => { const { components, utilities } = processPlugins( [ function({ addComponents, config }) { const containerClasses = [ { ".container": { width: "100%" } } ] _.forEach(config("screens"), breakpoint => { containerClasses.push({ [`@media (min-width: ${breakpoint})`]: { ".container": { maxWidth: breakpoint } } }) }) addComponents(containerClasses) } ], config({ screens: { sm: "576px", md: "768px", lg: "992px", xl: "1200px" } }) ) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` .container { width: 100% } @media (min-width: 576px) { .container { max-width: 576px } } @media (min-width: 768px) { .container { max-width: 768px } } @media (min-width: 992px) { .container { max-width: 992px } } @media (min-width: 1200px) { .container { max-width: 1200px } } `) }) test("plugins can provide fallbacks to keys missing from the config", () => { const { components, utilities } = processPlugins( [ function({ addComponents, config }) { addComponents({ ".btn": { borderRadius: config("borderRadius.default", ".25rem") } }) } ], config({ borderRadius: { "1": "1px", "2": "2px", "4": "4px", "8": "8px" } }) ) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` .btn { border-radius: .25rem } `) }) test("variants are optional when adding utilities", () => { const { utilities } = processPlugins( [ function({ addUtilities }) { addUtilities({ ".border-collapse": { "border-collapse": "collapse" } }) } ], config() ) expect(css(utilities)).toMatchCss(` @variants { .border-collapse { border-collapse: collapse } }`) }) test("plugins can add multiple sets of utilities and components", () => { const { components, utilities } = processPlugins( [ function({ addUtilities, addComponents }) { addComponents({ ".card": { padding: "1rem", borderRadius: ".25rem" } }) addUtilities({ ".skew-12deg": { transform: "skewY(-12deg)" } }) addComponents({ ".btn": { padding: "1rem .5rem", display: "inline-block" } }) addUtilities({ ".border-collapse": { borderCollapse: "collapse" } }) } ], config() ) expect(css(utilities)).toMatchCss(` @variants { .skew-12deg { transform: skewY(-12deg) } } @variants { .border-collapse { border-collapse: collapse } } `) expect(css(components)).toMatchCss(` .card { padding: 1rem; border-radius: .25rem } .btn { padding: 1rem .5rem; display: inline-block } `) }) test("plugins respect prefix and important options by default when adding utilities", () => { const { utilities } = processPlugins( [ function({ addUtilities }) { addUtilities({ ".rotate-90": { transform: "rotate(90deg)" } }) } ], config({ options: { prefix: "tw-", important: true } }) ) expect(css(utilities)).toMatchCss(` @variants { .tw-rotate-90 { transform: rotate(90deg) !important } } `) }) test("component declarations respect the 'prefix' option by default", () => { const { components } = processPlugins( [ function({ addComponents }) { addComponents({ ".btn-blue": { backgroundColor: "blue" } }) } ], config({ options: { prefix: "tw-" } }) ) expect(css(components)).toMatchCss(` .tw-btn-blue { background-color: blue } `) }) test("component declarations can optionally ignore 'prefix' option", () => { const { components } = processPlugins( [ function({ addComponents }) { addComponents( { ".btn-blue": { backgroundColor: "blue" } }, { respectPrefix: false } ) } ], config({ options: { prefix: "tw-" } }) ) expect(css(components)).toMatchCss(` .btn-blue { background-color: blue } `) }) test("component declarations are not affected by the 'important' option", () => { const { components } = processPlugins( [ function({ addComponents }) { addComponents({ ".btn-blue": { backgroundColor: "blue" } }) } ], config({ options: { important: true } }) ) expect(css(components)).toMatchCss(` .btn-blue { background-color: blue } `) }) test("plugins can apply the user's chosen prefix to components manually", () => { const { components } = processPlugins( [ function({ addComponents, prefix }) { addComponents( { [prefix(".btn-blue")]: { backgroundColor: "blue" } }, { respectPrefix: false } ) } ], config({ options: { prefix: "tw-" } }) ) expect(css(components)).toMatchCss(` .tw-btn-blue { background-color: blue } `) }) test("prefix can optionally be ignored for utilities", () => { const { utilities } = processPlugins( [ function({ addUtilities }) { addUtilities( { ".rotate-90": { transform: "rotate(90deg)" } }, { respectPrefix: false } ) } ], config({ options: { prefix: "tw-", important: true } }) ) expect(css(utilities)).toMatchCss(` @variants { .rotate-90 { transform: rotate(90deg) !important } } `) }) test("important can optionally be ignored for utilities", () => { const { utilities } = processPlugins( [ function({ addUtilities }) { addUtilities( { ".rotate-90": { transform: "rotate(90deg)" } }, { respectImportant: false } ) } ], config({ options: { prefix: "tw-", important: true } }) ) expect(css(utilities)).toMatchCss(` @variants { .tw-rotate-90 { transform: rotate(90deg) } } `) }) test("variants can still be specified when ignoring prefix and important options", () => { const { utilities } = processPlugins( [ function({ addUtilities }) { addUtilities( { ".rotate-90": { transform: "rotate(90deg)" } }, { variants: ["responsive", "hover", "focus"], respectImportant: false, respectPrefix: false } ) } ], config({ options: { prefix: "tw-", important: true } }) ) expect(css(utilities)).toMatchCss(` @variants responsive, hover, focus { .rotate-90 { transform: rotate(90deg) } } `) }) test("prefix will prefix all classes in a selector", () => { const { components } = processPlugins( [ function({ addComponents, prefix }) { addComponents( { [prefix(".btn-blue .w-1\\/4 > h1.text-xl + a .bar")]: { backgroundColor: "blue" } }, { respectPrefix: false } ) } ], config({ options: { prefix: "tw-" } }) ) expect(css(components)).toMatchCss(` .tw-btn-blue .tw-w-1\\/4 > h1.tw-text-xl + a .tw-bar { background-color: blue } `) })