diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 9c6b33253626..26ba8e295f4f 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -499,7 +499,7 @@ function registerPlugins(plugins, context) { pluginItem(pluginApi) } } else { - plugin(pluginApi) + plugin?.(pluginApi) } } diff --git a/tests/custom-plugins.test.js b/tests/custom-plugins.test.js new file mode 100644 index 000000000000..1256ff8b4f8f --- /dev/null +++ b/tests/custom-plugins.test.js @@ -0,0 +1,1894 @@ +import createPlugin from './../plugin' +import { run, html, css } from './util/run' + +test('plugins can create utilities with object syntax', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + plugins: [ + function ({ addUtilities }) { + addUtilities({ + '.custom-object-fill': { + 'object-fit': 'fill', + }, + '.custom-object-contain': { + 'object-fit': 'contain', + }, + '.custom-object-cover': { + 'object-fit': 'cover', + }, + }) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-object-fill { + object-fit: fill; + } + .custom-object-contain { + object-fit: contain; + } + .custom-object-cover { + object-fit: cover; + } + `) + }) +}) + +test('plugins can create utilities with arrays of objects', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function ({ addUtilities }) { + addUtilities([ + { + '.custom-object-fill': { + 'object-fit': 'fill', + }, + '.custom-object-contain': { + 'object-fit': 'contain', + }, + '.custom-object-cover': { + 'object-fit': 'cover', + }, + }, + ]) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-object-fill { + object-fit: fill; + } + .custom-object-contain { + object-fit: contain; + } + .custom-object-cover { + object-fit: cover; + } + `) + }) +}) + +test('plugins can create utilities with raw PostCSS nodes', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function ({ addUtilities, postcss }) { + addUtilities([ + postcss.rule({ selector: '.custom-object-fill' }).append([ + postcss.decl({ + prop: 'object-fit', + value: 'fill', + }), + ]), + postcss.rule({ selector: '.custom-object-contain' }).append([ + postcss.decl({ + prop: 'object-fit', + value: 'contain', + }), + ]), + postcss.rule({ selector: '.custom-object-cover' }).append([ + postcss.decl({ + prop: 'object-fit', + value: 'cover', + }), + ]), + ]) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-object-fill { + object-fit: fill; + } + .custom-object-contain { + object-fit: contain; + } + .custom-object-cover { + object-fit: cover; + } + `) + }) +}) + +test('plugins can create utilities with mixed object styles and PostCSS nodes', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function ({ addUtilities, postcss }) { + addUtilities([ + { + '.custom-object-fill': { + objectFit: 'fill', + }, + }, + postcss.rule({ selector: '.custom-object-contain' }).append([ + postcss.decl({ + prop: 'object-fit', + value: 'contain', + }), + ]), + postcss.rule({ selector: '.custom-object-cover' }).append([ + postcss.decl({ + prop: 'object-fit', + value: 'cover', + }), + ]), + ]) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-object-fill { + object-fit: fill; + } + .custom-object-contain { + object-fit: contain; + } + .custom-object-cover { + object-fit: cover; + } + `) + }) +}) + +test('plugins can create components with object syntax', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function ({ addComponents }) { + addComponents({ + '.btn-blue': { + backgroundColor: 'blue', + color: 'white', + padding: '.5rem 1rem', + borderRadius: '.25rem', + }, + '.btn-blue:hover': { + backgroundColor: 'darkblue', + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .btn-blue { + background-color: blue; + color: white; + padding: 0.5rem 1rem; + border-radius: 0.25rem; + } + .btn-blue:hover { + background-color: darkblue; + } + `) + }) +}) + +test('plugins can add base styles with object syntax', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function ({ addBase }) { + addBase({ + img: { + maxWidth: '100%', + }, + button: { + fontFamily: 'inherit', + }, + }) + }, + ], + corePlugins: { preflight: false }, + } + + return run('@tailwind base', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + img { + max-width: 100%; + } + button { + font-family: inherit; + } + `) + }) +}) + +test('plugins can add base styles with raw PostCSS nodes', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function ({ addBase, postcss }) { + addBase([ + postcss.rule({ selector: 'img' }).append([ + postcss.decl({ + prop: 'max-width', + value: '100%', + }), + ]), + postcss.rule({ selector: 'button' }).append([ + postcss.decl({ + prop: 'font-family', + value: 'inherit', + }), + ]), + ]) + }, + ], + corePlugins: { preflight: false }, + } + + return run('@tailwind base', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + img { + max-width: 100%; + } + button { + font-family: inherit; + } + `) + }) +}) + +test('plugins can create components with raw PostCSS nodes', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function ({ addComponents, postcss }) { + 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', + }), + ]), + ]) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .btn-blue { + background-color: blue; + color: white; + padding: 0.5rem 1rem; + border-radius: 0.25rem; + } + .btn-blue:hover { + background-color: darkblue; + } + `) + }) +}) + +test('plugins can create components with mixed object styles and raw PostCSS nodes', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function ({ addComponents, postcss }) { + 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', + }, + }, + ]) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .btn-blue { + background-color: blue; + color: white; + padding: 0.5rem 1rem; + border-radius: 0.25rem; + } + .btn-blue:hover { + background-color: darkblue; + } + `) + }) +}) + +test('plugins can create components with media queries with object syntax', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + function ({ addComponents }) { + addComponents({ + '.custom-container': { + width: '100%', + }, + '@media (min-width: 100px)': { + '.custom-container': { + maxWidth: '100px', + }, + }, + '@media (min-width: 200px)': { + '.custom-container': { + maxWidth: '200px', + }, + }, + '@media (min-width: 300px)': { + '.custom-container': { + maxWidth: '300px', + }, + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-container { + width: 100%; + } + @media (min-width: 100px) { + .custom-container { + max-width: 100px; + } + } + @media (min-width: 200px) { + .custom-container { + max-width: 200px; + } + } + @media (min-width: 300px) { + .custom-container { + max-width: 300px; + } + } + `) + }) +}) + +test('media queries can be defined multiple times using objects-in-array syntax', () => { + let config = { + content: [ + { + raw: html` + `, + }, + ], + plugins: [ + function ({ addComponents }) { + addComponents([ + { + '.custom-container': { + width: '100%', + }, + '@media (min-width: 100px)': { + '.custom-container': { + maxWidth: '100px', + }, + }, + }, + { + '.btn': { + padding: '1rem .5rem', + display: 'block', + }, + '@media (min-width: 100px)': { + '.btn': { + display: 'inline-block', + }, + }, + }, + ]) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-container { + width: 100%; + } + @media (min-width: 100px) { + .custom-container { + max-width: 100px; + } + } + .btn { + padding: 1rem 0.5rem; + display: block; + } + @media (min-width: 100px) { + .btn { + display: inline-block; + } + } + `) + }) +}) + +test('plugins can create nested rules', () => { + let config = { + content: [{ raw: html`` }], + plugins: [ + 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', + }, + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .btn-blue { + background-color: blue; + color: white; + padding: 0.5rem 1rem; + border-radius: 0.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', () => { + let config = { + content: [{ raw: html`` }], + plugins: [ + function ({ e, addUtilities }) { + addUtilities({ + [`.${e('custom-top-1/4')}`]: { + top: '25%', + }, + }) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-top-1\\/4 { + top: 25%; + } + `) + }) +}) + +test('plugins can access the current config', () => { + let config = { + content: [{ raw: html`` }], + plugins: [ + function ({ addComponents, config }) { + let containerClasses = [ + { + '.custom-container': { + width: '100%', + }, + }, + ] + + for (let maxWidth of Object.values(config('theme.customScreens'))) { + containerClasses.push({ + [`@media (min-width: ${maxWidth})`]: { + '.custom-container': { maxWidth }, + }, + }) + } + + addComponents(containerClasses) + }, + ], + theme: { + customScreens: { + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + }, + }, + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-container { + width: 100%; + } + @media (min-width: 576px) { + .custom-container { + max-width: 576px; + } + } + @media (min-width: 768px) { + .custom-container { + max-width: 768px; + } + } + @media (min-width: 992px) { + .custom-container { + max-width: 992px; + } + } + @media (min-width: 1200px) { + .custom-container { + max-width: 1200px; + } + } + `) + }) +}) + +test('plugins can check if corePlugins are enabled', () => { + let config = { + content: [{ raw: html`` }], + plugins: [ + function ({ addUtilities, corePlugins }) { + addUtilities({ + '.test': { + 'text-color': corePlugins('textColor') ? 'true' : 'false', + opacity: corePlugins('opacity') ? 'true' : 'false', + }, + }) + }, + ], + corePlugins: { textColor: false }, + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .test { + text-color: false; + opacity: true; + } + `) + }) +}) + +test('plugins can check if corePlugins are enabled when using array white-listing', () => { + let config = { + content: [{ raw: html`` }], + plugins: [ + function ({ addUtilities, corePlugins }) { + addUtilities({ + '.test': { + 'text-color': corePlugins('textColor') ? 'true' : 'false', + opacity: corePlugins('opacity') ? 'true' : 'false', + }, + }) + }, + ], + corePlugins: ['textColor'], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .test { + text-color: true; + opacity: false; + } + `) + }) +}) + +test('plugins can provide fallbacks to keys missing from the config', () => { + let config = { + content: [{ raw: html`` }], + plugins: [ + function ({ addComponents, config }) { + addComponents({ + '.btn': { + borderRadius: config('borderRadius.default', '.25rem'), + }, + }) + }, + ], + theme: { + borderRadius: { + 1: '1px', + 2: '2px', + 4: '4px', + 8: '8px', + }, + }, + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .btn { + border-radius: 0.25rem; + } + `) + }) +}) + +test('plugins can add multiple sets of utilities and components', () => { + let config = { + content: [ + { + raw: html` + `, + }, + ], + plugins: [ + function ({ addUtilities, addComponents }) { + addComponents({ + '.card': { + padding: '1rem', + borderRadius: '.25rem', + }, + }) + + addUtilities({ + '.custom-skew-12deg': { + transform: 'skewY(-12deg)', + }, + }) + + addComponents({ + '.btn': { + padding: '1rem .5rem', + display: 'inline-block', + }, + }) + + addUtilities({ + '.custom-border-collapse': { + borderCollapse: 'collapse', + }, + }) + }, + ], + } + + return run('@tailwind components; @tailwind utilities;', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .card { + padding: 1rem; + border-radius: 0.25rem; + } + .btn { + padding: 1rem 0.5rem; + display: inline-block; + } + .custom-skew-12deg { + transform: skewY(-12deg); + } + .custom-border-collapse { + border-collapse: collapse; + } + `) + }) +}) + +test('plugins respect prefix and important options by default when adding utilities', () => { + let config = { + prefix: 'tw-', + important: true, + content: [{ raw: html`` }], + plugins: [ + function ({ addUtilities }) { + addUtilities({ + '.custom-rotate-90': { + transform: 'rotate(90deg)', + }, + }) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .tw-custom-rotate-90 { + transform: rotate(90deg) !important; + } + `) + }) +}) + +test('when important is a selector it is used to scope utilities instead of adding !important', () => { + let config = { + prefix: 'tw-', + important: '#app', + content: [{ raw: html`` }], + plugins: [ + function ({ addUtilities }) { + addUtilities({ + '.custom-rotate-90': { + transform: 'rotate(90deg)', + }, + }) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + #app .tw-custom-rotate-90 { + transform: rotate(90deg); + } + `) + }) +}) + +test('when important is a selector it scopes all selectors in a rule, even though defining utilities like this is stupid', () => { + let config = { + important: '#app', + content: [{ raw: html`` }], + plugins: [ + function ({ addUtilities }) { + addUtilities({ + '.custom-rotate-90, .custom-rotate-1\\/4': { + transform: 'rotate(90deg)', + }, + }) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + #app .custom-rotate-90, + #app .custom-rotate-1\\/4 { + transform: rotate(90deg); + transform: rotate(90deg); + } + `) + }) +}) + +test('important utilities are not made double important when important option is used', () => { + let config = { + important: true, + content: [{ raw: html`` }], + plugins: [ + function ({ addUtilities }) { + addUtilities({ + '.custom-rotate-90': { + transform: 'rotate(90deg) !important', + }, + }) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-rotate-90 { + transform: rotate(90deg) !important; + } + `) + }) +}) + +test("component declarations respect the 'prefix' option by default", () => { + let config = { + prefix: 'tw-', + content: [{ raw: html`` }], + plugins: [ + function ({ addComponents }) { + addComponents({ + '.btn-blue': { + backgroundColor: 'blue', + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .tw-btn-blue { + background-color: blue; + } + `) + }) +}) + +test('all selectors in a rule are prefixed', () => { + let config = { + prefix: 'tw-', + content: [ + { + raw: html` + `, + }, + ], + plugins: [ + function ({ addUtilities, addComponents }) { + addUtilities({ + '.custom-rotate-90, .custom-rotate-1\\/4': { + transform: 'rotate(90deg)', + }, + }) + addComponents({ + '.btn-blue, .btn-red': { + padding: '10px', + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .tw-btn-blue, + .tw-btn-red { + padding: 10px; + padding: 10px; + } + `) + }) +}) + +test("component declarations can optionally ignore 'prefix' option", () => { + let config = { + prefix: 'tw-', + content: [{ raw: html`` }], + plugins: [ + function ({ addComponents }) { + addComponents( + { + '.btn-blue': { + backgroundColor: 'blue', + }, + }, + { respectPrefix: false } + ) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .btn-blue { + background-color: blue; + } + `) + }) +}) + +test("component declarations are not affected by the 'important' option", () => { + let config = { + important: true, + content: [{ raw: html`` }], + plugins: [ + function ({ addComponents }) { + addComponents({ + '.btn-blue': { + backgroundColor: 'blue', + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .btn-blue { + background-color: blue; + } + `) + }) +}) + +test("plugins can apply the user's chosen prefix to components manually", () => { + let config = { + prefix: 'tw-', + content: [{ raw: html`` }], + plugins: [ + function ({ addComponents, prefix }) { + addComponents( + { + [prefix('.btn-blue')]: { + backgroundColor: 'blue', + }, + }, + { respectPrefix: false } + ) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .tw-btn-blue { + background-color: blue; + } + `) + }) +}) + +test('prefix can optionally be ignored for utilities', () => { + let config = { + prefix: 'tw-', + content: [{ raw: html`` }], + plugins: [ + function ({ addUtilities }) { + addUtilities( + { + '.custom-rotate-90': { + transform: 'rotate(90deg)', + }, + }, + { respectPrefix: false } + ) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-rotate-90 { + transform: rotate(90deg); + } + `) + }) +}) + +test('important can optionally be ignored for utilities', () => { + let config = { + important: true, + content: [{ raw: html`` }], + plugins: [ + function ({ addUtilities }) { + addUtilities( + { + '.custom-rotate-90': { + transform: 'rotate(90deg)', + }, + }, + { respectImportant: false } + ) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-rotate-90 { + transform: rotate(90deg); + } + `) + }) +}) + +test('prefix will prefix all classes in a selector', () => { + let config = { + prefix: 'tw-', + content: [{ raw: html`` }], + plugins: [ + function ({ addComponents, prefix }) { + addComponents( + { + [prefix('.btn-blue .w-1\\/4 > h1.text-xl + a .bar')]: { + backgroundColor: 'blue', + }, + }, + { respectPrefix: false } + ) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .tw-btn-blue .tw-w-1\\/4 > h1.tw-text-xl + a .tw-bar { + background-color: blue; + } + `) + }) +}) + +test('plugins can be provided as an object with a handler function', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + { + handler({ addUtilities }) { + addUtilities({ + '.custom-object-fill': { + 'object-fit': 'fill', + }, + '.custom-object-contain': { + 'object-fit': 'contain', + }, + '.custom-object-cover': { + 'object-fit': 'cover', + }, + }) + }, + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .custom-object-fill { + object-fit: fill; + } + .custom-object-contain { + object-fit: contain; + } + .custom-object-cover { + object-fit: cover; + } + `) + }) +}) + +test('plugins can provide a config but no handler', () => { + let config = { + content: [ + { + raw: html``, + }, + ], + plugins: [ + { + config: { + prefix: 'tw-', + }, + }, + { + handler({ addUtilities }) { + addUtilities({ + '.custom-object-fill': { + 'object-fit': 'fill', + }, + '.custom-object-contain': { + 'object-fit': 'contain', + }, + '.custom-object-cover': { + 'object-fit': 'cover', + }, + }) + }, + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .tw-custom-object-fill { + object-fit: fill; + } + .tw-custom-object-contain { + object-fit: contain; + } + .tw-custom-object-cover { + object-fit: cover; + } + `) + }) +}) + +test('plugins can be created using the `createPlugin` function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + screens: { + sm: '400px', + }, + }, + plugins: [ + createPlugin( + function ({ addUtilities, theme, variants }) { + addUtilities( + Object.fromEntries( + Object.entries(theme('testPlugin')).map(([k, v]) => [ + `.test-${k}`, + { testProperty: v }, + ]) + ), + variants('testPlugin') + ) + }, + { + theme: { + testPlugin: { + sm: '1rem', + md: '2rem', + lg: '3rem', + }, + }, + } + ), + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .test-sm { + test-property: 1rem; + } + .test-md { + test-property: 2rem; + } + .test-lg { + test-property: 3rem; + } + .hover\\:test-sm:hover { + test-property: 1rem; + } + @media (min-width: 400px) { + .sm\\:test-sm { + test-property: 1rem; + } + } + `) + }) +}) + +test('plugins with extra options can be created using the `createPlugin.withOptions` function', () => { + let plugin = createPlugin.withOptions( + function ({ className }) { + return function ({ addUtilities, theme }) { + addUtilities( + Object.fromEntries( + Object.entries(theme('testPlugin')).map(([k, v]) => [ + `.${className}-${k}`, + { testProperty: v }, + ]) + ) + ) + } + }, + function () { + return { + theme: { + testPlugin: { + sm: '1rem', + md: '2rem', + lg: '3rem', + }, + }, + } + } + ) + + let config = { + content: [ + { raw: html`` }, + ], + corePlugins: [], + theme: { + screens: { + sm: '400px', + }, + }, + plugins: [plugin({ className: 'banana' })], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .banana-sm { + test-property: 1rem; + } + .banana-md { + test-property: 2rem; + } + .banana-lg { + test-property: 3rem; + } + .hover\\:banana-sm:hover { + test-property: 1rem; + } + @media (min-width: 400px) { + .sm\\:banana-sm { + test-property: 1rem; + } + } + `) + }) +}) + +test('plugins should cache correctly', () => { + let plugin = createPlugin.withOptions(({ className = 'banana' } = {}) => ({ addComponents }) => { + addComponents({ [`.${className}`]: { position: 'absolute' } }) + }) + + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + screens: { + sm: '400px', + }, + }, + } + + function internalRun(options = {}) { + return run('@tailwind components', { + ...config, + plugins: [plugin(options)], + }) + } + + return Promise.all([internalRun(), internalRun({ className: 'apple' })]).then( + ([result1, result2]) => { + let expected1 = css` + .banana { + position: absolute; + } + + @media (min-width: 400px) { + .sm\\:banana { + position: absolute; + } + } + ` + + let expected2 = css` + .apple { + position: absolute; + } + + @media (min-width: 400px) { + .sm\\:apple { + position: absolute; + } + } + ` + + expect(result1.css).toMatchCss(expected1) + expect(result2.css).toMatchCss(expected2) + } + ) +}) + +test('plugins created using `createPlugin.withOptions` do not need to be invoked if the user wants to use the default options', () => { + let plugin = createPlugin.withOptions( + function ({ className } = { className: 'banana' }) { + return function ({ addUtilities, theme }) { + addUtilities( + Object.fromEntries( + Object.entries(theme('testPlugin')).map(([k, v]) => [ + `.${className}-${k}`, + { testProperty: v }, + ]) + ) + ) + } + }, + function () { + return { + theme: { + testPlugin: { + sm: '1rem', + md: '2rem', + lg: '3rem', + }, + }, + variants: { + testPlugin: ['responsive', 'hover'], + }, + } + } + ) + + let config = { + content: [ + { raw: html`` }, + ], + corePlugins: [], + theme: { + screens: { + sm: '400px', + }, + }, + plugins: [plugin], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .banana-sm { + test-property: 1rem; + } + .banana-md { + test-property: 2rem; + } + .banana-lg { + test-property: 3rem; + } + .hover\\:banana-sm:hover { + test-property: 1rem; + } + @media (min-width: 400px) { + .sm\\:banana-sm { + test-property: 1rem; + } + } + `) + }) +}) + +test('the configFunction parameter is optional when using the `createPlugin.withOptions` function', () => { + let plugin = createPlugin.withOptions(function ({ className }) { + return function ({ addUtilities, theme }) { + addUtilities( + Object.fromEntries( + Object.entries(theme('testPlugin')).map(([k, v]) => [ + `.${className}-${k}`, + { testProperty: v }, + ]) + ) + ) + } + }) + + let config = { + content: [ + { raw: html`` }, + ], + corePlugins: [], + theme: { + screens: { + sm: '400px', + }, + testPlugin: { + sm: '1px', + md: '2px', + lg: '3px', + }, + }, + plugins: [plugin({ className: 'banana' })], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .banana-sm { + test-property: 1px; + } + .banana-md { + test-property: 2px; + } + .banana-lg { + test-property: 3px; + } + .hover\\:banana-sm:hover { + test-property: 1px; + } + @media (min-width: 400px) { + .sm\\:banana-sm { + test-property: 1px; + } + } + `) + }) +}) + +test('keyframes are not escaped', () => { + let config = { + content: [{ raw: html`` }], + plugins: [ + function ({ matchUtilities }) { + matchUtilities({ + foo: (value, { includeRules }) => { + includeRules( + [ + { + [`@keyframes ${value}`]: { + '25.001%': { + color: 'black', + }, + }, + }, + ], + { respectImportant: false } + ) + + return { + animation: `${value} 1s infinite`, + } + }, + }) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + @keyframes abc { + 25.001% { + color: black; + } + } + .foo-\\[abc\\] { + animation: abc 1s infinite; + } + `) + }) +}) + +test('font sizes are retrieved without default line-heights or letter-spacing using theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + fontSize: { + sm: ['14px', '20px'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + fontSize: theme('fontSize.sm'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + font-size: 14px; + } + `) + }) +}) + +test('outlines are retrieved without outline-offset using theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + outline: { + black: ['2px dotted black', '4px'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + outline: theme('outline.black'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + outline: 2px dotted black; + } + `) + }) +}) + +test('box-shadow values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + boxShadow: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + boxShadow: theme('boxShadow.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + box-shadow: width, height; + } + `) + }) +}) + +test('transition-property values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + transitionProperty: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + transitionProperty: theme('transitionProperty.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + transition-property: width, height; + } + `) + }) +}) + +test('transition-duration values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + transitionDuration: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + transitionDuration: theme('transitionDuration.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + transition-duration: width, height; + } + `) + }) +}) + +test('transition-delay values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + transitionDuration: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + transitionDuration: theme('transitionDuration.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + transition-duration: width, height; + } + `) + }) +}) + +test('transition-timing-function values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + transitionTimingFunction: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + transitionTimingFunction: theme('transitionTimingFunction.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + transition-timing-function: width, height; + } + `) + }) +}) + +test('background-image values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + backgroundImage: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + backgroundImage: theme('backgroundImage.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + background-image: width, height; + } + `) + }) +}) + +test('background-size values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + backgroundSize: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + backgroundSize: theme('backgroundSize.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + background-size: width, height; + } + `) + }) +}) + +test('background-color values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + backgroundColor: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + backgroundColor: theme('backgroundColor.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + background-color: width, height; + } + `) + }) +}) + +test('cursor values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + cursor: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + cursor: theme('cursor.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + cursor: width, height; + } + `) + }) +}) + +test('animation values are joined when retrieved using the theme function', () => { + let config = { + content: [{ raw: html`` }], + corePlugins: [], + theme: { + animation: { + lol: ['width', 'height'], + }, + }, + plugins: [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + animation: theme('animation.lol'), + }, + }) + }, + ], + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .foo { + animation: width, height; + } + `) + }) +})