From ef325ea35b6f201e2096316e9b0bf94b09bb3a0d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 16 Nov 2021 18:01:06 +0100 Subject: [PATCH] Add tuple syntax to guarantee screens order (#6104) * add normalizeScreens function This will allow us to normalize the various kinds of inputs to a stable version that is consistent regardless of the input. * use normalized screens * add dedicated test for new tuple syntax * make test consistent with other tests While working on the normalizeScreens feature, some tests started failing (the one with multiple screens), while looking at them I made them consistent with the rest of the codebase. * add test to ensure consistent order in screens output * update changelog * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: Adam Wathan --- CHANGELOG.md | 3 +- src/corePlugins.js | 49 +-- src/lib/evaluateTailwindFunctions.js | 7 +- src/lib/substituteScreenAtRules.js | 9 +- src/util/buildMediaQuery.js | 32 +- src/util/normalizeScreens.js | 42 +++ tests/containerPlugin.test.js | 36 +++ tests/evaluateTailwindFunctions.test.js | 401 ++++++++++++++++-------- tests/normalize-screens.test.js | 98 ++++++ tests/tailwind-screens.test.js | 48 +++ 10 files changed, 531 insertions(+), 194 deletions(-) create mode 100644 src/util/normalizeScreens.js create mode 100644 tests/normalize-screens.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 15756dfd6394..d1965c00b164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -Nothing yet! - ### Fixed - Enforce the order of some variants (like `before` and `after`) ([#6018](https://github.com/tailwindlabs/tailwindcss/pull/6018)) @@ -16,6 +14,7 @@ Nothing yet! ### Added - Add `placeholder` variant ([#6106](https://github.com/tailwindlabs/tailwindcss/pull/6106)) +- Add tuple syntax for configuring screens while guaranteeing order ([#5956](https://github.com/tailwindlabs/tailwindcss/pull/5956)) ## [3.0.0-alpha.2] - 2021-11-08 diff --git a/src/corePlugins.js b/src/corePlugins.js index dc76e279dc4c..831358270858 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -11,6 +11,7 @@ import isPlainObject from './util/isPlainObject' import transformThemeValue from './util/transformThemeValue' import { version as tailwindVersion } from '../package.json' import log from './util/log' +import { normalizeScreens } from './util/normalizeScreens' import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue' export let variantPlugins = { @@ -158,11 +159,10 @@ export let variantPlugins = { }, screenVariants: ({ theme, addVariant }) => { - for (let screen in theme('screens')) { - let size = theme('screens')[screen] - let query = buildMediaQuery(size) + for (let screen of normalizeScreens(theme('screens'))) { + let query = buildMediaQuery(screen) - addVariant(screen, `@media ${query}`) + addVariant(screen.name, `@media ${query}`) } }, } @@ -182,24 +182,10 @@ export let corePlugins = { }, container: (() => { - function extractMinWidths(breakpoints) { - return Object.values(breakpoints ?? {}).flatMap((breakpoints) => { - if (typeof breakpoints === 'string') { - breakpoints = { min: breakpoints } - } - - if (!Array.isArray(breakpoints)) { - breakpoints = [breakpoints] - } - - return breakpoints - .filter((breakpoint) => { - return breakpoint?.hasOwnProperty?.('min') || breakpoint?.hasOwnProperty('min-width') - }) - .map((breakpoint) => { - return breakpoint['min-width'] ?? breakpoint.min - }) - }) + function extractMinWidths(breakpoints = []) { + return breakpoints + .flatMap((breakpoint) => breakpoint.values.map((breakpoint) => breakpoint.min)) + .filter((v) => v !== undefined) } function mapMinWidthsToPadding(minWidths, screens, paddings) { @@ -228,16 +214,11 @@ export let corePlugins = { } for (let minWidth of minWidths) { - for (let [screen, value] of Object.entries(screens)) { - let screenMinWidth = - typeof value === 'object' && value !== null ? value.min || value['min-width'] : value - - if (`${screenMinWidth}` === `${minWidth}`) { - mapping.push({ - screen, - minWidth, - padding: paddings[screen], - }) + for (let screen of screens) { + for (let { min } of screen.values) { + if (min === minWidth) { + mapping.push({ minWidth, padding: paddings[screen.name] }) + } } } } @@ -246,12 +227,12 @@ export let corePlugins = { } return function ({ addComponents, theme }) { - let screens = theme('container.screens', theme('screens')) + let screens = normalizeScreens(theme('container.screens', theme('screens'))) let minWidths = extractMinWidths(screens) let paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding')) let generatePaddingFor = (minWidth) => { - let paddingConfig = paddings.find((padding) => `${padding.minWidth}` === `${minWidth}`) + let paddingConfig = paddings.find((padding) => padding.minWidth === minWidth) if (!paddingConfig) { return {} diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js index fb3474b2cfa3..778230e59817 100644 --- a/src/lib/evaluateTailwindFunctions.js +++ b/src/lib/evaluateTailwindFunctions.js @@ -2,6 +2,7 @@ import dlv from 'dlv' import didYouMean from 'didyoumean' import transformThemeValue from '../util/transformThemeValue' import parseValue from 'postcss-value-parser' +import { normalizeScreens } from '../util/normalizeScreens' import buildMediaQuery from '../util/buildMediaQuery' import { toPath } from '../util/toPath' @@ -173,12 +174,14 @@ export default function ({ tailwindConfig: config }) { }, screen: (node, screen) => { screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '') + let screens = normalizeScreens(config.theme.screens) + let screenDefinition = screens.find(({ name }) => name === screen) - if (config.theme.screens[screen] === undefined) { + if (!screenDefinition) { throw node.error(`The '${screen}' screen does not exist in your theme.`) } - return buildMediaQuery(config.theme.screens[screen]) + return buildMediaQuery(screenDefinition) }, } return (root) => { diff --git a/src/lib/substituteScreenAtRules.js b/src/lib/substituteScreenAtRules.js index 1b61d38f35d6..5a45cff222ef 100644 --- a/src/lib/substituteScreenAtRules.js +++ b/src/lib/substituteScreenAtRules.js @@ -1,16 +1,19 @@ +import { normalizeScreens } from '../util/normalizeScreens' import buildMediaQuery from '../util/buildMediaQuery' export default function ({ tailwindConfig: { theme } }) { return function (css) { css.walkAtRules('screen', (atRule) => { - const screen = atRule.params + let screen = atRule.params + let screens = normalizeScreens(theme.screens) + let screenDefinition = screens.find(({ name }) => name === screen) - if (!theme.screens?.hasOwnProperty?.(screen)) { + if (!screenDefinition) { throw atRule.error(`No \`${screen}\` screen found.`) } atRule.name = 'media' - atRule.params = buildMediaQuery(theme.screens[screen]) + atRule.params = buildMediaQuery(screenDefinition) }) } } diff --git a/src/util/buildMediaQuery.js b/src/util/buildMediaQuery.js index ba3b696e889b..ccfc63baf099 100644 --- a/src/util/buildMediaQuery.js +++ b/src/util/buildMediaQuery.js @@ -1,24 +1,20 @@ export default function buildMediaQuery(screens) { - if (typeof screens === 'string') { - screens = { min: screens } - } - - if (!Array.isArray(screens)) { - screens = [screens] - } + screens = Array.isArray(screens) ? screens : [screens] return screens - .map((screen) => { - if (screen?.hasOwnProperty?.('raw')) { - return screen.raw - } + .map((screen) => + screen.values.map((screen) => { + if (screen.raw !== undefined) { + return screen.raw + } - return Object.entries(screen) - .map(([feature, value]) => { - feature = { min: 'min-width', max: 'max-width' }[feature] ?? feature - return `(${feature}: ${value})` - }) - .join(' and ') - }) + return [ + screen.min && `(min-width: ${screen.min})`, + screen.max && `(max-width: ${screen.max})`, + ] + .filter(Boolean) + .join(' and ') + }) + ) .join(', ') } diff --git a/src/util/normalizeScreens.js b/src/util/normalizeScreens.js new file mode 100644 index 000000000000..e7bcefe6b24f --- /dev/null +++ b/src/util/normalizeScreens.js @@ -0,0 +1,42 @@ +/** + * A function that normalizes the various forms that the screens object can be + * provided in. + * + * Input(s): + * - ['100px', '200px'] // Raw strings + * - { sm: '100px', md: '200px' } // Object with string values + * - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values + * - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values) + * - [['sm', '100px'], ['md', '200px']] // Tuple object + * + * Output(s): + * - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values + */ +export function normalizeScreens(screens) { + if (Array.isArray(screens)) { + return screens.map((screen) => { + if (typeof screen === 'string') { + return { name: screen.toString(), values: [{ min: screen, max: undefined }] } + } + + let [name, options] = screen + name = name.toString() + + if (typeof options === 'string') { + return { name, values: [{ min: options, max: undefined }] } + } + + if (Array.isArray(options)) { + return { name, values: options.map((option) => resolveValue(option)) } + } + + return { name, values: [resolveValue(options)] } + }) + } + + return normalizeScreens(Object.entries(screens ?? {})) +} + +function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) { + return { min, max, raw } +} diff --git a/tests/containerPlugin.test.js b/tests/containerPlugin.test.js index 51b7093b9b22..7dfc73521613 100644 --- a/tests/containerPlugin.test.js +++ b/tests/containerPlugin.test.js @@ -343,3 +343,39 @@ test('container can use variants', () => { `) }) }) + +test('container can use screens with tuple syntax', () => { + let config = { + content: [{ raw: html`
` }], + theme: { + screens: [ + [1800, { min: '1800px' }], + [1200, { min: '1200px' }], + [768, { min: '768px' }], + ], + }, + } + + return run('@tailwind components', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .container { + width: 100%; + } + @media (min-width: 768px) { + .container { + max-width: 768px; + } + } + @media (min-width: 1200px) { + .container { + max-width: 1200px; + } + } + @media (min-width: 1800px) { + .container { + max-width: 1800px; + } + } + `) + }) +}) diff --git a/tests/evaluateTailwindFunctions.test.js b/tests/evaluateTailwindFunctions.test.js index b2c69a0a144b..01d7393ba5bc 100644 --- a/tests/evaluateTailwindFunctions.test.js +++ b/tests/evaluateTailwindFunctions.test.js @@ -1,17 +1,22 @@ import postcss from 'postcss' import plugin from '../src/lib/evaluateTailwindFunctions' +import { css } from './util/run' function run(input, opts = {}) { return postcss([plugin({ tailwindConfig: opts })]).process(input, { from: undefined }) } test('it looks up values in the theme using dot notation', () => { - const input = ` - .banana { color: theme('colors.yellow'); } + let input = css` + .banana { + color: theme('colors.yellow'); + } ` - const output = ` - .banana { color: #f7cc50; } + let output = css` + .banana { + color: #f7cc50; + } ` return run(input, { @@ -27,37 +32,85 @@ test('it looks up values in the theme using dot notation', () => { }) test('color can be a function', () => { - const input = ` - .backgroundColor { color: theme('backgroundColor.fn') } - .borderColor { color: theme('borderColor.fn') } - .caretColor { color: theme('caretColor.fn') } - .colors { color: theme('colors.fn') } - .divideColor { color: theme('divideColor.fn') } - .fill { color: theme('fill.fn') } - .gradientColorStops { color: theme('gradientColorStops.fn') } - .placeholderColor { color: theme('placeholderColor.fn') } - .ringColor { color: theme('ringColor.fn') } - .ringOffsetColor { color: theme('ringOffsetColor.fn') } - .stroke { color: theme('stroke.fn') } - .textColor { color: theme('textColor.fn') } - ` - - const output = ` - .backgroundColor { color: #f00 } - .borderColor { color: #f00 } - .caretColor { color: #f00 } - .colors { color: #f00 } - .divideColor { color: #f00 } - .fill { color: #f00 } - .gradientColorStops { color: #f00 } - .placeholderColor { color: #f00 } - .ringColor { color: #f00 } - .ringOffsetColor { color: #f00 } - .stroke { color: #f00 } - .textColor { color: #f00 } - ` - - const fn = () => `#f00` + let input = css` + .backgroundColor { + color: theme('backgroundColor.fn'); + } + .borderColor { + color: theme('borderColor.fn'); + } + .caretColor { + color: theme('caretColor.fn'); + } + .colors { + color: theme('colors.fn'); + } + .divideColor { + color: theme('divideColor.fn'); + } + .fill { + color: theme('fill.fn'); + } + .gradientColorStops { + color: theme('gradientColorStops.fn'); + } + .placeholderColor { + color: theme('placeholderColor.fn'); + } + .ringColor { + color: theme('ringColor.fn'); + } + .ringOffsetColor { + color: theme('ringOffsetColor.fn'); + } + .stroke { + color: theme('stroke.fn'); + } + .textColor { + color: theme('textColor.fn'); + } + ` + + let output = css` + .backgroundColor { + color: #f00; + } + .borderColor { + color: #f00; + } + .caretColor { + color: #f00; + } + .colors { + color: #f00; + } + .divideColor { + color: #f00; + } + .fill { + color: #f00; + } + .gradientColorStops { + color: #f00; + } + .placeholderColor { + color: #f00; + } + .ringColor { + color: #f00; + } + .ringOffsetColor { + color: #f00; + } + .stroke { + color: #f00; + } + .textColor { + color: #f00; + } + ` + + let fn = () => `#f00` return run(input, { theme: { @@ -81,12 +134,16 @@ test('color can be a function', () => { }) test('quotes are optional around the lookup path', () => { - const input = ` - .banana { color: theme(colors.yellow); } + let input = css` + .banana { + color: theme(colors.yellow); + } ` - const output = ` - .banana { color: #f7cc50; } + let output = css` + .banana { + color: #f7cc50; + } ` return run(input, { @@ -102,12 +159,16 @@ test('quotes are optional around the lookup path', () => { }) test('a default value can be provided', () => { - const input = ` - .cookieMonster { color: theme('colors.blue', #0000ff); } + let input = css` + .cookieMonster { + color: theme('colors.blue', #0000ff); + } ` - const output = ` - .cookieMonster { color: #0000ff; } + let output = css` + .cookieMonster { + color: #0000ff; + } ` return run(input, { @@ -123,12 +184,16 @@ test('a default value can be provided', () => { }) test('the default value can use the theme function', () => { - const input = ` - .cookieMonster { color: theme('colors.blue', theme('colors.yellow')); } + let input = css` + .cookieMonster { + color: theme('colors.blue', theme('colors.yellow')); + } ` - const output = ` - .cookieMonster { color: #f7cc50; } + let output = css` + .cookieMonster { + color: #f7cc50; + } ` return run(input, { @@ -144,12 +209,16 @@ test('the default value can use the theme function', () => { }) test('quotes are preserved around default values', () => { - const input = ` - .heading { font-family: theme('fontFamily.sans', "Helvetica Neue"); } + let input = css` + .heading { + font-family: theme('fontFamily.sans', 'Helvetica Neue'); + } ` - const output = ` - .heading { font-family: "Helvetica Neue"; } + let output = css` + .heading { + font-family: 'Helvetica Neue'; + } ` return run(input, { @@ -165,12 +234,16 @@ test('quotes are preserved around default values', () => { }) test('an unquoted list is valid as a default value', () => { - const input = ` - .heading { font-family: theme('fontFamily.sans', Helvetica, Arial, sans-serif); } + let input = css` + .heading { + font-family: theme('fontFamily.sans', Helvetica, Arial, sans-serif); + } ` - const output = ` - .heading { font-family: Helvetica, Arial, sans-serif; } + let output = css` + .heading { + font-family: Helvetica, Arial, sans-serif; + } ` return run(input, { @@ -186,8 +259,10 @@ test('an unquoted list is valid as a default value', () => { }) test('a missing root theme value throws', () => { - const input = ` - .heading { color: theme('colours.gray.100'); } + let input = css` + .heading { + color: theme('colours.gray.100'); + } ` return expect( @@ -204,8 +279,10 @@ test('a missing root theme value throws', () => { }) test('a missing nested theme property throws', () => { - const input = ` - .heading { color: theme('colors.red'); } + let input = css` + .heading { + color: theme('colors.red'); + } ` return expect( @@ -223,8 +300,10 @@ test('a missing nested theme property throws', () => { }) test('a missing nested theme property with a close alternative throws with a suggestion', () => { - const input = ` - .heading { color: theme('colors.yellw'); } + let input = css` + .heading { + color: theme('colors.yellw'); + } ` return expect( @@ -241,8 +320,10 @@ test('a missing nested theme property with a close alternative throws with a sug }) test('a path through a non-object throws', () => { - const input = ` - .heading { color: theme('colors.yellow.100'); } + let input = css` + .heading { + color: theme('colors.yellow.100'); + } ` return expect( @@ -259,8 +340,10 @@ test('a path through a non-object throws', () => { }) test('a path which exists but is not a string throws', () => { - const input = ` - .heading { color: theme('colors.yellow'); } + let input = css` + .heading { + color: theme('colors.yellow'); + } ` return expect( @@ -275,8 +358,10 @@ test('a path which exists but is not a string throws', () => { }) test('a path which exists but is invalid throws', () => { - const input = ` - .heading { color: theme('colors'); } + let input = css` + .heading { + color: theme('colors'); + } ` return expect( @@ -289,8 +374,10 @@ test('a path which exists but is invalid throws', () => { }) test('a path which is an object throws with a suggested key', () => { - const input = ` - .heading { color: theme('colors'); } + let input = css` + .heading { + color: theme('colors'); + } ` return expect( @@ -307,12 +394,16 @@ test('a path which is an object throws with a suggested key', () => { }) test('array values are joined by default', () => { - const input = ` - .heading { font-family: theme('fontFamily.sans'); } + let input = css` + .heading { + font-family: theme('fontFamily.sans'); + } ` - const output = ` - .heading { font-family: Inter, Helvetica, sans-serif; } + let output = css` + .heading { + font-family: Inter, Helvetica, sans-serif; + } ` return run(input, { @@ -328,14 +419,22 @@ test('array values are joined by default', () => { }) test('font sizes are retrieved without default line-heights or letter-spacing', () => { - const input = ` - .heading-1 { font-size: theme('fontSize.lg'); } - .heading-2 { font-size: theme('fontSize.xl'); } + let input = css` + .heading-1 { + font-size: theme('fontSize.lg'); + } + .heading-2 { + font-size: theme('fontSize.xl'); + } ` - const output = ` - .heading-1 { font-size: 20px; } - .heading-2 { font-size: 24px; } + let output = css` + .heading-1 { + font-size: 20px; + } + .heading-2 { + font-size: 24px; + } ` return run(input, { @@ -352,12 +451,16 @@ test('font sizes are retrieved without default line-heights or letter-spacing', }) test('outlines are retrieved without default outline-offset', () => { - const input = ` - .element { outline: theme('outline.black'); } + let input = css` + .element { + outline: theme('outline.black'); + } ` - const output = ` - .element { outline: 2px dotted black; } + let output = css` + .element { + outline: 2px dotted black; + } ` return run(input, { @@ -373,12 +476,16 @@ test('outlines are retrieved without default outline-offset', () => { }) test('font-family values are joined when an array', () => { - const input = ` - .element { font-family: theme('fontFamily.sans'); } + let input = css` + .element { + font-family: theme('fontFamily.sans'); + } ` - const output = ` - .element { font-family: Helvetica, Arial, sans-serif; } + let output = css` + .element { + font-family: Helvetica, Arial, sans-serif; + } ` return run(input, { @@ -394,12 +501,16 @@ test('font-family values are joined when an array', () => { }) test('box-shadow values are joined when an array', () => { - const input = ` - .element { box-shadow: theme('boxShadow.wtf'); } + let input = css` + .element { + box-shadow: theme('boxShadow.wtf'); + } ` - const output = ` - .element { box-shadow: 0 0 2px black, 1px 2px 3px white; } + let output = css` + .element { + box-shadow: 0 0 2px black, 1px 2px 3px white; + } ` return run(input, { @@ -415,12 +526,16 @@ test('box-shadow values are joined when an array', () => { }) test('transition-property values are joined when an array', () => { - const input = ` - .element { transition-property: theme('transitionProperty.colors'); } + let input = css` + .element { + transition-property: theme('transitionProperty.colors'); + } ` - const output = ` - .element { transition-property: color, fill; } + let output = css` + .element { + transition-property: color, fill; + } ` return run(input, { @@ -436,12 +551,16 @@ test('transition-property values are joined when an array', () => { }) test('transition-duration values are joined when an array', () => { - const input = ` - .element { transition-duration: theme('transitionDuration.lol'); } + let input = css` + .element { + transition-duration: theme('transitionDuration.lol'); + } ` - const output = ` - .element { transition-duration: 1s, 2s; } + let output = css` + .element { + transition-duration: 1s, 2s; + } ` return run(input, { @@ -457,16 +576,18 @@ test('transition-duration values are joined when an array', () => { }) test('basic screen function calls are expanded', () => { - const input = ` + let input = css` @media screen(sm) { - .foo {} + .foo { + } } ` - const output = ` - @media (min-width: 600px) { - .foo {} - } + let output = css` + @media (min-width: 600px) { + .foo { + } + } ` return run(input, { @@ -478,16 +599,18 @@ test('basic screen function calls are expanded', () => { }) test('screen function supports max-width screens', () => { - const input = ` + let input = css` @media screen(sm) { - .foo {} + .foo { + } } ` - const output = ` - @media (max-width: 600px) { - .foo {} - } + let output = css` + @media (max-width: 600px) { + .foo { + } + } ` return run(input, { @@ -499,16 +622,18 @@ test('screen function supports max-width screens', () => { }) test('screen function supports min-width screens', () => { - const input = ` + let input = css` @media screen(sm) { - .foo {} + .foo { + } } ` - const output = ` - @media (min-width: 600px) { - .foo {} - } + let output = css` + @media (min-width: 600px) { + .foo { + } + } ` return run(input, { @@ -520,16 +645,18 @@ test('screen function supports min-width screens', () => { }) test('screen function supports min-width and max-width screens', () => { - const input = ` + let input = css` @media screen(sm) { - .foo {} + .foo { + } } ` - const output = ` - @media (min-width: 600px) and (max-width: 700px) { - .foo {} - } + let output = css` + @media (min-width: 600px) and (max-width: 700px) { + .foo { + } + } ` return run(input, { @@ -541,16 +668,18 @@ test('screen function supports min-width and max-width screens', () => { }) test('screen function supports raw screens', () => { - const input = ` + let input = css` @media screen(mono) { - .foo {} + .foo { + } } ` - const output = ` - @media monochrome { - .foo {} - } + let output = css` + @media monochrome { + .foo { + } + } ` return run(input, { @@ -562,16 +691,18 @@ test('screen function supports raw screens', () => { }) test('screen arguments can be quoted', () => { - const input = ` + let input = css` @media screen('sm') { - .foo {} + .foo { + } } ` - const output = ` - @media (min-width: 600px) { - .foo {} - } + let output = css` + @media (min-width: 600px) { + .foo { + } + } ` return run(input, { diff --git a/tests/normalize-screens.test.js b/tests/normalize-screens.test.js new file mode 100644 index 000000000000..1a5b4a886a55 --- /dev/null +++ b/tests/normalize-screens.test.js @@ -0,0 +1,98 @@ +import { normalizeScreens } from '../src/util/normalizeScreens' + +it('should normalize an array of string values', () => { + let screens = ['768px', '1200px'] + + expect(normalizeScreens(screens)).toEqual([ + { name: '768px', values: [{ min: '768px', max: undefined }] }, + { name: '1200px', values: [{ min: '1200px', max: undefined }] }, + ]) +}) + +it('should normalize an object with string values', () => { + let screens = { + a: '768px', + b: '1200px', + } + + expect(normalizeScreens(screens)).toEqual([ + { name: 'a', values: [{ min: '768px', max: undefined }] }, + { name: 'b', values: [{ min: '1200px', max: undefined }] }, + ]) +}) + +it('should normalize an object with object values', () => { + let screens = { + a: { min: '768px' }, + b: { max: '1200px' }, + } + + expect(normalizeScreens(screens)).toEqual([ + { name: 'a', values: [{ min: '768px', max: undefined }] }, + { name: 'b', values: [{ min: undefined, max: '1200px' }] }, + ]) +}) + +it('should normalize an object with multiple object values', () => { + let screens = { + a: [{ min: '768px' }, { max: '1200px' }], + } + + expect(normalizeScreens(screens)).toEqual([ + { + name: 'a', + values: [ + { max: undefined, min: '768px', raw: undefined }, + { max: '1200px', min: undefined, raw: undefined }, + ], + }, + ]) +}) + +it('should normalize an object with object values (min-width normalized to width)', () => { + let screens = { + a: { 'min-width': '768px' }, + b: { max: '1200px' }, + } + + expect(normalizeScreens(screens)).toEqual([ + { name: 'a', values: [{ min: '768px', max: undefined }] }, + { name: 'b', values: [{ min: undefined, max: '1200px' }] }, + ]) +}) + +it('should normalize a tuple with string values', () => { + let screens = [ + ['a', '768px'], + ['b', '1200px'], + ] + + expect(normalizeScreens(screens)).toEqual([ + { name: 'a', values: [{ min: '768px', max: undefined }] }, + { name: 'b', values: [{ min: '1200px', max: undefined }] }, + ]) +}) + +it('should normalize a tuple with object values', () => { + let screens = [ + ['a', { min: '768px' }], + ['b', { max: '1200px' }], + ] + + expect(normalizeScreens(screens)).toEqual([ + { name: 'a', values: [{ min: '768px', max: undefined }] }, + { name: 'b', values: [{ min: undefined, max: '1200px' }] }, + ]) +}) + +it('should normalize a tuple with object values (min-width normalized to width)', () => { + let screens = [ + ['a', { 'min-width': '768px' }], + ['b', { max: '1200px' }], + ] + + expect(normalizeScreens(screens)).toEqual([ + { name: 'a', values: [{ min: '768px', max: undefined }] }, + { name: 'b', values: [{ min: undefined, max: '1200px' }] }, + ]) +}) diff --git a/tests/tailwind-screens.test.js b/tests/tailwind-screens.test.js index 784bf40c7af7..232ae0113771 100644 --- a/tests/tailwind-screens.test.js +++ b/tests/tailwind-screens.test.js @@ -65,3 +65,51 @@ test('`@tailwind screens` works as an alias for `@tailwind variants`', async () `) }) }) + +test('screen names are in the correct order', () => { + // Using custom css function here, because otherwise with String.raw, we run + // into an issue with `\31 ` escapes. If we use `\31 ` then JS complains + // about strict mode. But `\\31 ` is not what it expected. + function css(templates) { + return templates.join('') + } + + let config = { + content: [ + { + raw: html`
`, + }, + ], + theme: { + screens: [ + [1800, { max: '1800px' }], + [1200, { max: '1200px' }], + [768, { max: '768px' }], + ], + }, + } + + return run('@tailwind utilities;', config).then((result) => { + return expect(result.css).toMatchFormattedCss(css` + @media (max-width: 1800px) { + .\\31 800\\:font-bold { + font-weight: 700; + } + } + + @media (max-width: 1200px) { + .\\31 200\\:font-normal { + font-weight: 400; + } + } + + @media (max-width: 768px) { + .\\37 68\\:font-light { + font-weight: 300; + } + } + `) + }) +})