diff --git a/jest/customMatchers.js b/jest/customMatchers.js new file mode 100644 index 0000000..c60acee --- /dev/null +++ b/jest/customMatchers.js @@ -0,0 +1,146 @@ +const prettier = require('prettier') +const { diff } = require('jest-diff') + +function format(input) { + return prettier.format(input, { + parser: 'css', + printWidth: 100, + }) +} + +expect.extend({ + // Compare two CSS strings with all whitespace removed + // This is probably naive but it's fast and works well enough. + toMatchCss(received, argument) { + function stripped(str) { + return str.replace(/\s/g, '').replace(/;/g, '') + } + + const options = { + comment: 'stripped(received) === stripped(argument)', + isNot: this.isNot, + promise: this.promise, + } + + const pass = stripped(received) === stripped(argument) + + const message = pass + ? () => { + return ( + this.utils.matcherHint('toMatchCss', undefined, undefined, options) + + '\n\n' + + `Expected: not ${this.utils.printExpected(format(received))}\n` + + `Received: ${this.utils.printReceived(format(argument))}` + ) + } + : () => { + const actual = format(received) + const expected = format(argument) + + const diffString = diff(expected, actual, { + expand: this.expand, + }) + + return ( + this.utils.matcherHint('toMatchCss', undefined, undefined, options) + + '\n\n' + + (diffString && diffString.includes('- Expect') + ? `Difference:\n\n${diffString}` + : `Expected: ${this.utils.printExpected(expected)}\n` + + `Received: ${this.utils.printReceived(actual)}`) + ) + } + + return { actual: received, message, pass } + }, + toIncludeCss(received, argument) { + const options = { + comment: 'stripped(received).includes(stripped(argument))', + isNot: this.isNot, + promise: this.promise, + } + + const actual = format(received) + const expected = format(argument) + + const pass = actual.includes(expected) + + const message = pass + ? () => { + return ( + this.utils.matcherHint('toIncludeCss', undefined, undefined, options) + + '\n\n' + + `Expected: not ${this.utils.printExpected(format(received))}\n` + + `Received: ${this.utils.printReceived(format(argument))}` + ) + } + : () => { + const diffString = diff(expected, actual, { + expand: this.expand, + }) + + return ( + this.utils.matcherHint('toIncludeCss', undefined, undefined, options) + + '\n\n' + + (diffString && diffString.includes('- Expect') + ? `Difference:\n\n${diffString}` + : `Expected: ${this.utils.printExpected(expected)}\n` + + `Received: ${this.utils.printReceived(actual)}`) + ) + } + + return { actual: received, message, pass } + }, +}) + +expect.extend({ + // Compare two CSS strings with all whitespace removed + // This is probably naive but it's fast and works well enough. + toMatchFormattedCss(received, argument) { + function format(input) { + return prettier.format(input.replace(/\n/g, ''), { + parser: 'css', + printWidth: 100, + }) + } + const options = { + comment: 'stripped(received) === stripped(argument)', + isNot: this.isNot, + promise: this.promise, + } + + let formattedReceived = format(received) + let formattedArgument = format(argument) + + const pass = formattedReceived === formattedArgument + + const message = pass + ? () => { + return ( + this.utils.matcherHint('toMatchCss', undefined, undefined, options) + + '\n\n' + + `Expected: not ${this.utils.printExpected(formattedReceived)}\n` + + `Received: ${this.utils.printReceived(formattedArgument)}` + ) + } + : () => { + const actual = formattedReceived + const expected = formattedArgument + + const diffString = diff(expected, actual, { + expand: this.expand, + }) + + return ( + this.utils.matcherHint('toMatchCss', undefined, undefined, options) + + '\n\n' + + (diffString && diffString.includes('- Expect') + ? `Difference:\n\n${diffString}` + : `Expected: ${this.utils.printExpected(expected)}\n` + + `Received: ${this.utils.printReceived(actual)}`) + ) + } + + return { actual: received, message, pass } + }, +}) \ No newline at end of file diff --git a/package.json b/package.json index 885962a..f1f1735 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,20 @@ "singleQuote": true, "trailingComma": "es5" }, + "scripts": { + "test": "jest" + }, + "devDependencies": { + "jest": "^27.4.4", + "postcss": "^8.2.2", + "prettier": "^2.5.1" + }, "peerDependencies": { "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" + }, + "jest": { + "setupFilesAfterEnv": [ + "/jest/customMatchers.js" + ] } } diff --git a/src/index.js b/src/index.js index f354e23..2d5a978 100644 --- a/src/index.js +++ b/src/index.js @@ -1,21 +1,27 @@ const plugin = require('tailwindcss/plugin') +const baseStyles = { + overflow: 'hidden', + display: '-webkit-box', + '-webkit-box-orient': 'vertical', +} + const lineClamp = plugin( - function ({ addUtilities, theme, variants, e }) { + function ({ matchUtilities, addUtilities, theme, variants, e }) { const values = theme('lineClamp') + matchUtilities( + { + 'line-clamp': (value) => ({ + ...baseStyles, + '-webkit-line-clamp': `${value}`, + }) + }, + { values } + ) + addUtilities( [ - Object.entries(values).map(([key, value]) => { - return { - [`.${e(`line-clamp-${key}`)}`]: { - overflow: 'hidden', - display: '-webkit-box', - '-webkit-box-orient': 'vertical', - '-webkit-line-clamp': `${value}`, - }, - } - }), { '.line-clamp-none': { '-webkit-line-clamp': 'unset', diff --git a/src/index.test.js b/src/index.test.js new file mode 100644 index 0000000..92de2f6 --- /dev/null +++ b/src/index.test.js @@ -0,0 +1,61 @@ +const path = require('path') +const postcss = require('postcss') +const tailwindcss = require('tailwindcss') +const lineClampPlugin = require('.') + +function run(config, plugin = tailwindcss) { + let { currentTestName } = expect.getState() + config = { + ...{ plugins: [lineClampPlugin], corePlugins: { preflight: false } }, + ...config, + } + + return postcss(plugin(config)).process('@tailwind utilities', { + from: `${path.resolve(__filename)}?test=${currentTestName}`, + }) +} + +it('should add the `line-clamp-{n}` components', () => { + const config = { + content: [{ raw: String.raw`
` }], + } + + return run(config).then((result) => { + expect(result.css).toMatchCss(String.raw` + .line-clamp-2 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + + .line-clamp-\[33\] { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 33; + } + + .line-clamp-\[var\(--line-clamp-variable\)\] { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: var(--line-clamp-variable); + } + `) + }) +}) + +it('should add the `line-clamp-none` utility', () => { + const config = { + content: [{ raw: String.raw`
` }], + } + + return run(config).then((result) => { + expect(result.css).toMatchCss(String.raw` + .line-clamp-none { + -webkit-line-clamp: unset; + } + `) + }) +}) \ No newline at end of file