import postcss from 'postcss' import plugin from '../src/lib/substituteVariantsAtRules' import processPlugins from '../src/util/processPlugins' import resolveConfig from '../src/util/resolveConfig' import config from '../stubs/defaultConfig.stub.js' function run(input, opts = config) { const resolved = resolveConfig([opts]) return postcss([plugin(resolved, processPlugins(resolved.plugins, resolved))]).process(input, { from: undefined, }) } test('it can generate hover variants', () => { const input = ` @variants hover { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .hover\\:banana:hover { color: yellow; } .hover\\:chocolate:hover { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate disabled variants', () => { const input = ` @variants disabled { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .disabled\\:banana:disabled { color: yellow; } .disabled\\:chocolate:disabled { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate checked variants', () => { const input = ` @variants checked { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .checked\\:banana:checked { color: yellow; } .checked\\:chocolate:checked { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate active variants', () => { const input = ` @variants active { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .active\\:banana:active { color: yellow; } .active\\:chocolate:active { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate visited variants', () => { const input = ` @variants visited { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .visited\\:banana:visited { color: yellow; } .visited\\:chocolate:visited { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate focus variants', () => { const input = ` @variants focus { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .focus\\:banana:focus { color: yellow; } .focus\\:chocolate:focus { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate focus-within variants', () => { const input = ` @variants focus-within { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .focus-within\\:banana:focus-within { color: yellow; } .focus-within\\:chocolate:focus-within { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate focus-visible variants', () => { const input = ` @variants focus-visible { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .focus-visible\\:banana:focus-visible { color: yellow; } .focus-visible\\:chocolate:focus-visible { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate motion-reduce variants', () => { const input = ` @variants motion-reduce { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } @media (prefers-reduced-motion: reduce) { .motion-reduce\\:banana { color: yellow; } .motion-reduce\\:chocolate { color: brown; } } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate motion-safe variants', () => { const input = ` @variants motion-safe { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } @media (prefers-reduced-motion: no-preference) { .motion-safe\\:banana { color: yellow; } .motion-safe\\:chocolate { color: brown; } } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate motion-safe and motion-reduce variants', () => { const input = ` @variants motion-safe, motion-reduce { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } @media (prefers-reduced-motion: no-preference) { .motion-safe\\:banana { color: yellow; } .motion-safe\\:chocolate { color: brown; } } @media (prefers-reduced-motion: reduce) { .motion-reduce\\:banana { color: yellow; } .motion-reduce\\:chocolate { color: brown; } } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('motion-reduce variants stack with basic variants', () => { const input = ` @variants motion-reduce, hover, group-hover { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .hover\\:banana:hover { color: yellow; } .hover\\:chocolate:hover { color: brown; } .group:hover .group-hover\\:banana { color: yellow; } .group:hover .group-hover\\:chocolate { color: brown; } @media (prefers-reduced-motion: reduce) { .motion-reduce\\:banana { color: yellow; } .motion-reduce\\:chocolate { color: brown; } .motion-reduce\\:hover\\:banana:hover { color: yellow; } .motion-reduce\\:hover\\:chocolate:hover { color: brown; } .group:hover .motion-reduce\\:group-hover\\:banana { color: yellow; } .group:hover .motion-reduce\\:group-hover\\:chocolate { color: brown; } } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('motion-safe variants stack with basic variants', () => { const input = ` @variants motion-safe, hover, group-hover { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .hover\\:banana:hover { color: yellow; } .hover\\:chocolate:hover { color: brown; } .group:hover .group-hover\\:banana { color: yellow; } .group:hover .group-hover\\:chocolate { color: brown; } @media (prefers-reduced-motion: no-preference) { .motion-safe\\:banana { color: yellow; } .motion-safe\\:chocolate { color: brown; } .motion-safe\\:hover\\:banana:hover { color: yellow; } .motion-safe\\:hover\\:chocolate:hover { color: brown; } .group:hover .motion-safe\\:group-hover\\:banana { color: yellow; } .group:hover .motion-safe\\:group-hover\\:chocolate { color: brown; } } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('motion-safe and motion-reduce variants stack with basic variants', () => { const input = ` @variants motion-reduce, motion-safe, hover { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .hover\\:banana:hover { color: yellow; } .hover\\:chocolate:hover { color: brown; } @media (prefers-reduced-motion: reduce) { .motion-reduce\\:banana { color: yellow; } .motion-reduce\\:chocolate { color: brown; } .motion-reduce\\:hover\\:banana:hover { color: yellow; } .motion-reduce\\:hover\\:chocolate:hover { color: brown; } } @media (prefers-reduced-motion: no-preference) { .motion-safe\\:banana { color: yellow; } .motion-safe\\:chocolate { color: brown; } .motion-safe\\:hover\\:banana:hover { color: yellow; } .motion-safe\\:hover\\:chocolate:hover { color: brown; } } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate first-child variants', () => { const input = ` @variants first { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .first\\:banana:first-child { color: yellow; } .first\\:chocolate:first-child { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate odd variants', () => { const input = ` @variants odd { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .odd\\:banana:nth-child(odd) { color: yellow; } .odd\\:chocolate:nth-child(odd) { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate last-child variants', () => { const input = ` @variants last { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .last\\:banana:last-child { color: yellow; } .last\\:chocolate:last-child { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate even variants', () => { const input = ` @variants even { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .even\\:banana:nth-child(even) { color: yellow; } .even\\:chocolate:nth-child(even) { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate group-hover variants', () => { const input = ` @variants group-hover { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .group:hover .group-hover\\:banana { color: yellow; } .group:hover .group-hover\\:chocolate { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('group-hover variants respect any configured prefix', () => { const input = ` @variants group-hover { .tw-banana { color: yellow; } .tw-chocolate { color: brown; } } ` const output = ` .tw-banana { color: yellow; } .tw-chocolate { color: brown; } .tw-group:hover .group-hover\\:tw-banana { color: yellow; } .tw-group:hover .group-hover\\:tw-chocolate { color: brown; } ` return run(input, { ...config, prefix: 'tw-', }).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate group-focus variants', () => { const input = ` @variants group-focus { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .group:focus .group-focus\\:banana { color: yellow; } .group:focus .group-focus\\:chocolate { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('group-focus variants respect any configured prefix', () => { const input = ` @variants group-focus { .tw-banana { color: yellow; } .tw-chocolate { color: brown; } } ` const output = ` .tw-banana { color: yellow; } .tw-chocolate { color: brown; } .tw-group:focus .group-focus\\:tw-banana { color: yellow; } .tw-group:focus .group-focus\\:tw-chocolate { color: brown; } ` return run(input, { ...config, prefix: 'tw-', }).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate hover, active and focus variants', () => { const input = ` @variants group-hover, hover, focus, active { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .group:hover .group-hover\\:banana { color: yellow; } .group:hover .group-hover\\:chocolate { color: brown; } .hover\\:banana:hover { color: yellow; } .hover\\:chocolate:hover { color: brown; } .focus\\:banana:focus { color: yellow; } .focus\\:chocolate:focus { color: brown; } .active\\:banana:active { color: yellow; } .active\\:chocolate:active { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it can generate hover, active and focus variants for multiple classes in one rule', () => { const input = ` @variants hover, focus, active { .banana, .lemon { color: yellow; } .chocolate, .coconut { color: brown; } } ` const output = ` .banana, .lemon { color: yellow; } .chocolate, .coconut { color: brown; } .hover\\:banana:hover, .hover\\:lemon:hover { color: yellow; } .hover\\:chocolate:hover, .hover\\:coconut:hover { color: brown; } .focus\\:banana:focus, .focus\\:lemon:focus { color: yellow; } .focus\\:chocolate:focus, .focus\\:coconut:focus { color: brown; } .active\\:banana:active, .active\\:lemon:active { color: yellow; } .active\\:chocolate:active, .active\\:coconut:active { color: brown; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('it wraps the output in a responsive at-rule if responsive is included as a variant', () => { const input = ` @variants responsive, hover, focus { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` @responsive { .banana { color: yellow; } .chocolate { color: brown; } .hover\\:banana:hover { color: yellow; } .hover\\:chocolate:hover { color: brown; } .focus\\:banana:focus { color: yellow; } .focus\\:chocolate:focus { color: brown; } } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('variants are generated in the order specified', () => { const input = ` @variants focus, active, hover { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .focus\\:banana:focus { color: yellow; } .focus\\:chocolate:focus { color: brown; } .active\\:banana:active { color: yellow; } .active\\:chocolate:active { color: brown; } .hover\\:banana:hover { color: yellow; } .hover\\:chocolate:hover { color: brown; } ` return run(input, { ...config, }).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('the built-in variant pseudo-selectors are appended before any pseudo-elements', () => { const input = ` @variants hover, focus-within, focus-visible, focus, active, group-hover { .placeholder-yellow::placeholder { color: yellow; } } ` const output = ` .placeholder-yellow::placeholder { color: yellow; } .hover\\:placeholder-yellow:hover::placeholder { color: yellow; } .focus-within\\:placeholder-yellow:focus-within::placeholder { color: yellow; } .focus-visible\\:placeholder-yellow:focus-visible::placeholder { color: yellow; } .focus\\:placeholder-yellow:focus::placeholder { color: yellow; } .active\\:placeholder-yellow:active::placeholder { color: yellow; } .group:hover .group-hover\\:placeholder-yellow::placeholder { color: yellow; } ` return run(input).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('the default variant can be generated in a specified position', () => { const input = ` @variants focus, active, DEFAULT, hover { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .focus\\:banana:focus { color: yellow; } .focus\\:chocolate:focus { color: brown; } .active\\:banana:active { color: yellow; } .active\\:chocolate:active { color: brown; } .banana { color: yellow; } .chocolate { color: brown; } .hover\\:banana:hover { color: yellow; } .hover\\:chocolate:hover { color: brown; } ` return run(input, { ...config, }).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('nested rules are not modified', () => { const input = ` @variants focus, active, hover { .banana { color: yellow; .chocolate { color: brown; } } } ` const output = ` .banana { color: yellow; .chocolate { color: brown; } } .focus\\:banana:focus { color: yellow; .chocolate { color: brown; } } .active\\:banana:active { color: yellow; .chocolate { color: brown; } } .hover\\:banana:hover { color: yellow; .chocolate { color: brown; } } ` return run(input, { ...config, }).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('plugin variants can modify rules using the raw PostCSS API', () => { const input = ` @variants important { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .\\!banana { color: yellow !important; } .\\!chocolate { color: brown !important; } ` return run(input, { ...config, plugins: [ ...config.plugins, function ({ addVariant }) { addVariant('important', ({ container }) => { container.walkRules((rule) => { rule.selector = `.\\!${rule.selector.slice(1)}` rule.walkDecls((decl) => { decl.important = true }) }) }) }, ], }).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('plugin variants can modify selectors with a simplified API', () => { const input = ` @variants first-child { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } .first-child\\:banana:first-child { color: yellow; } .first-child\\:chocolate:first-child { color: brown; } ` return run(input, { ...config, plugins: [ ...config.plugins, function ({ addVariant, e }) { addVariant('first-child', ({ modifySelectors, separator }) => { modifySelectors(({ className }) => { return `.${e(`first-child${separator}${className}`)}:first-child` }) }) }, ], }).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('plugin variants that use modify selectors need to manually escape the class name they are modifying', () => { const input = ` @variants first-child { .banana-1\\/2 { color: yellow; } .chocolate-1\\.5 { color: brown; } } ` const output = ` .banana-1\\/2 { color: yellow; } .chocolate-1\\.5 { color: brown; } .first-child\\:banana-1\\/2:first-child { color: yellow; } .first-child\\:chocolate-1\\.5:first-child { color: brown; } ` return run(input, { ...config, plugins: [ ...config.plugins, function ({ addVariant, e }) { addVariant('first-child', ({ modifySelectors, separator }) => { modifySelectors(({ className }) => { return `.${e(`first-child${separator}${className}`)}:first-child` }) }) }, ], }).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) test('plugin variants can wrap rules in another at-rule using the raw PostCSS API', () => { const input = ` @variants supports-grid { .banana { color: yellow; } .chocolate { color: brown; } } ` const output = ` .banana { color: yellow; } .chocolate { color: brown; } @supports (display: grid) { .supports-grid\\:banana { color: yellow; } .supports-grid\\:chocolate { color: brown; } } ` return run(input, { ...config, plugins: [ ...config.plugins, function ({ addVariant, e }) { addVariant('supports-grid', ({ container, separator }) => { const supportsRule = postcss.atRule({ name: 'supports', params: '(display: grid)', }) supportsRule.nodes = container.nodes container.nodes = [supportsRule] supportsRule.walkRules((rule) => { rule.selector = `.${e(`supports-grid${separator}${rule.selector.slice(1)}`)}` }) }) }, ], }).then((result) => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) })