diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 0c13fb006d..13a845fca4 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -10,7 +10,6 @@ - [Plugins](developer-guide/plugins.md): Writing your own plugins. - [Processors](developer-guide/processors.md): Writing your own processors. - [Formatters](developer-guide/formatters.md): Writing your own formatters. -- [Rule testers](developer-guide/rule-testers.md): Using and writing rule testers. ## Core team guides diff --git a/docs/developer-guide/rule-testers.md b/docs/developer-guide/rule-testers.md deleted file mode 100644 index da143864a5..0000000000 --- a/docs/developer-guide/rule-testers.md +++ /dev/null @@ -1,90 +0,0 @@ -# Rule testers - -stylelint rules require *a lot* of tests. So we've built a specialized stylelint rule testing format to speed up the mass production of consistent, effective rule tests. - -There is a schema for describing tests, and a function for creating "rule testers" that interpret that schema using a test framework (e.g. tape or Mocha). - -When developing plugins, you can use the following rule testers or create your own. - -- stylelint-test-rule-tape -- stylelint-test-rule-mocha -- stylelint-test-rule-ava - -## Using a rule tester - -To use the rule tester of your choice, do the following: - -```js -// `testRule` = the imported rule tester -testRule(rule, testGroupDescription) -``` - -`rule` is just the rule that you are testing (a function). - -`testGroupDescription` is an object fitting the following schema. - -### The test group schema - -Each test group object describes a set of test-cases for a certain rule with a certain configuration. - -Required properties: - -- `ruleName` {string}: The name of the rule. Used in generated test-case descriptions. -- `config` {any}: The rule's configuration for this test group. Should match the rule configuration format you'd use in `.stylelintrc`. -- `accept` {array}: An array of objects describing test cases that *should not violate the rule*. Each object has these properties: - - `code` {string}: The source CSS to check. - - `description` {string}: *Optional.* A description of the case. - - `only` {boolean}: If `true`, run only this test case. -- `reject` {array}: An array of objects describing test cases that *should violate the rule once*. Each object has these properties: - - `code` {string}: The source CSS to check. - - `message` {string}: The message of the expected violation. - - `line` {number}: *Optional but recommended.* The expected line number of the violation. If this is left out, the line won't be checked. - - `column` {number}: *Optional but recommended.* The expected column number of the violation. If this is left out, the column won't be checked. - - `description` {string}: *Optional.* A description of the case. - - `only` {boolean}: If `true`, run only this test case. - - `fixed` {string}: *Required if test schema has `fix` enabled.* Result of autofixing against `code` property. - -Optional properties: - -- `syntax` {"css"|"css-in-js"|"html"|"less"|"markdown"|"sass"|"scss"|"sugarss"}: Defaults to `"css"`. Other settings use special parsers. -- `skipBasicChecks` {boolean}: Defaults to `false`. If `true`, a few rudimentary checks (that should almost always be included) will not be performed. You can check those out in `lib/testUtils/basicChecks.js`. -- `preceedingPlugins` {array}: An array of PostCSS plugins that should be run before the CSS is tested. -- `fix` {boolean}: Defaults to `false`. If `true`, every `reject` test-case will be tested for autofixing functionality. *Required if rule has autofixing.* - -## Creating a rule tester - -stylelint itself exposes a means of creating rule testers with just about any testing framework. - -```js -var testRule = stylelint.createRuleTester(equalityCheck) -``` - -Pass in an `equalityCheck` function. Given some information, this checker should use whatever test runner you like to perform equality checks. - -The `equalityCheck` function should accept two arguments: - -- `processCss` {Promise}: A Promise that resolves with an array of comparisons that you need to check (documented below). -- `context` {object}: An object that contains additional information you may need: - - `caseDescription` {string}: A description of the test case as whole. It will end up printing like something this: - ```bash - > rule: value-list-comma-space-before - > config: "always-single-line" - > code: "a { background-size: 0 ,0;\n}" - ``` - - `comparisonCount` {number}: The number of comparisons that will need to be performed (e.g. useful for tape). - - `completeAssertionDescription` {string}: While each individual comparison may have its own description, this is a description of the whole assertion (e.g. useful for Mocha). - - `only` {boolean}: If `true`, the test runner should only run this test case (e.g. `test.only` in tape, `describe.only` in Mocha). - -`processCss` is a Promise that resolves with an array of comparisons. Each comparison has the following properties: - -- `actual` {any}: Some actual value. -- `expected` {any}: Some expected value. -- `description` {string}: A (possibly empty) description of the comparison. - -Within the `equalityCheck` function, you need to ensure that you do the following: - -- Set up the test case. -- When `processCss` resolves, loop through every comparison. -- For each comparison, make an assertion checking that `actual === expected`. - -A `testRule` function (as described above) is returned. diff --git a/lib/index.js b/lib/index.js index c1d565fb7f..7c9da54cd5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,7 +2,6 @@ const checkAgainstRule = require('./utils/checkAgainstRule'); const createPlugin = require('./createPlugin'); -const createRuleTester = require('./testUtils/createRuleTester'); const createStylelint = require('./createStylelint'); const formatters = require('./formatters'); const postcssPlugin = require('./postcssPlugin'); @@ -44,7 +43,6 @@ api.lint = standalone; api.rules = requiredRules; api.formatters = formatters; api.createPlugin = createPlugin; -api.createRuleTester = createRuleTester; api.createLinter = createStylelint; module.exports = api; diff --git a/lib/testUtils/__tests__/createRuleTester.test.js b/lib/testUtils/__tests__/createRuleTester.test.js deleted file mode 100644 index f7091d11fc..0000000000 --- a/lib/testUtils/__tests__/createRuleTester.test.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -const createRuleTester = require('../createRuleTester'); - -function createRuleTesterPromise(rule, schema) { - const contexts = []; - - return new Promise((resolve, reject) => { - const ruleTester = createRuleTester((promise, res) => { - contexts.push(res); - promise.then(resolve, reject); - }); - - ruleTester(rule, schema); - }).then(() => contexts); -} - -describe('createRuleTester', () => { - it('is possible to create a tester', () => { - const schema = { - ruleName: 'my-test-rule', - accept: [], - }; - - const rule = jest.fn((root, result) => { - expect(root).toBeDefined(); - expect(result).toBeDefined(); - }); - - return createRuleTesterPromise(() => rule, schema).then((contexts) => { - expect(contexts).toEqual([ - { - comparisonCount: 1, - caseDescription: '\n> rule: my-test-rule\n> config: \n> code: ""\n', - completeAssertionDescription: 'empty stylesheet should be accepted', - }, - { - comparisonCount: 1, - caseDescription: '\n> rule: my-test-rule\n> config: \n> code: "a {}"\n', - completeAssertionDescription: 'empty rule should be accepted', - }, - { - comparisonCount: 1, - caseDescription: '\n> rule: my-test-rule\n> config: \n> code: "@import \\"foo.css\\";"\n', - completeAssertionDescription: 'blockless statement should be accepted', - }, - { - comparisonCount: 1, - caseDescription: '\n> rule: my-test-rule\n> config: \n> code: ":global {}"\n', - completeAssertionDescription: 'CSS Modules global empty rule set should be accepted', - }, - ]); - expect(rule).toHaveBeenCalledTimes(4); - }); - }); - - it('is possible to pass preceeding plugins to a tester', () => { - const postCssPlugin = jest.fn((id) => id); - const schema = { - preceedingPlugins: [postCssPlugin], - }; - - return createRuleTesterPromise(() => () => {}, schema).then(() => { - expect(postCssPlugin).toHaveBeenCalledTimes(4); - }); - }); -}); diff --git a/lib/testUtils/createRuleTester.js b/lib/testUtils/createRuleTester.js deleted file mode 100644 index c4a040041b..0000000000 --- a/lib/testUtils/createRuleTester.js +++ /dev/null @@ -1,306 +0,0 @@ -//@ts-nocheck -'use strict'; - -const _ = require('lodash'); -const assignDisabledRanges = require('../assignDisabledRanges'); -const basicChecks = require('./basicChecks'); -const lessSyntax = require('postcss-less'); -const normalizeRuleSettings = require('../normalizeRuleSettings'); -const postcss = require('postcss'); -const sassSyntax = require('postcss-sass'); -const scssSyntax = require('postcss-scss'); -const sugarss = require('sugarss'); -const { deprecate } = require('util'); - -/** - * Create a stylelint rule testing function. - * - * Pass in an `equalityCheck` function. Given some information, - * this checker should use Whatever Test Runner to perform - * equality checks. - * - * `equalityCheck` should accept two arguments: - * - `processCss` {Promise}: A Promise that resolves with an array of - * comparisons that you need to check (documented below). - * - `context` {object}: An object that contains additional information - * you may need: - * - `caseDescription` {string}: A description of the test case as a whole. - * Will look like this: - * > rule: value-list-comma-space-before - * > config: "always-single-line" - * > code: "a { background-size: 0 ,0;\n}" - * - `comparisonCount` {number}: The number of comparisons that - * will need to be performed (e.g. useful for tape). - * - `completeAssertionDescription` {string}: While each individual - * comparison may have its own description, this is a description - * of the whole assertion (e.g. useful for Mocha). - * - `only` {boolean}: If `true`, the test runner should only run this - * test case (e.g. `test.only` in tape, `describe.only` in Mocha). - * - * `processCss` is a Promsie that resolves with an array of comparisons. - * Each comparison has the following properties: - * - `actual` {any}: Some actual value. - * - `expected` {any}: Some expected value. - * - `description` {string}: A (possibly empty) description of the comparison. - * - * Within `equalityCheck`, you need to ensure that you: - * - Set up the test case. - * - When `processCss` resolves, loop through every comparison. - * - For each comparison, make an assertion checking that `actual === expected`. - * - * The `testRule` function that you get has a simple signature: - * `testRule(rule, testGroupDescription)`. - * - * `rule` is just the rule that you are testing (a function). - * - * `testGroupDescription` is an object fitting the following schema. - * - * Required properties: - * - `ruleName` {string}: The name of the rule. Used in descriptions. - * - `config` {any}: The rule's configuration for this test group. - * Should match the format you'd use in `.stylelintrc`. - * - `accept` {array}: An array of objects describing test cases that - * should not violate the rule. Each object has these properties: - * - `code` {string}: The source CSS to check. - * - `description` {[string]}: An optional description of the case. - * - `reject` {array}: An array of objects describing test cases that - * should violate the rule once. Each object has these properties: - * - `code` {string}: The source CSS to check. - * - `message` {string}: The message of the expected violation. - * - `line` {[number]}: The expected line number of the violation. - * If this is left out, the line won't be checked. - * - `column` {[number]}: The expected column number of the violation. - * If this is left out, the column won't be checked. - * - `description` {[string]}: An optional description of the case. - * - * Optional properties: - * - `syntax` {"css"|"scss"|"less"|"sugarss"}: Defaults to `"css"`. - * - `skipBasicChecks` {boolean}: Defaults to `false`. If `true`, a - * few rudimentary checks (that should almost always be included) - * will not be performed. - * - `preceedingPlugins` {array}: An array of PostCSS plugins that - * should be run before the CSS is tested. - * - * @param {function} equalityCheck - Described above - * @return {function} testRule - Decsribed above - */ -let onlyTest; - -function checkCaseForOnly(caseType, testCase) { - if (!testCase.only) { - return; - } - - /* istanbul ignore next */ - if (onlyTest) { - throw new Error('Cannot use `only` on multiple test cases'); - } - - onlyTest = { case: testCase, type: caseType }; -} - -function createRuleTester(equalityCheck) { - return function(rule, schema) { - const alreadyHadOnlyTest = !!onlyTest; - - if (schema.accept) { - schema.accept.forEach(_.partial(checkCaseForOnly, 'accept')); - } - - if (schema.reject) { - schema.reject.forEach(_.partial(checkCaseForOnly, 'reject')); - } - - if (onlyTest) { - schema = _.assign(_.omit(schema, ['accept', 'reject']), { - skipBasicChecks: true, - [onlyTest.type]: [onlyTest.case], - }); - } - - if (!alreadyHadOnlyTest) { - process.nextTick(() => { - processGroup(rule, schema, equalityCheck); - }); - } - }; -} - -function processGroup(rule, schema, equalityCheck) { - const ruleName = schema.ruleName; - - const ruleOptions = normalizeRuleSettings(schema.config, ruleName); - const rulePrimaryOptions = ruleOptions[0]; - const ruleSecondaryOptions = ruleOptions[1]; - - let printableConfig = rulePrimaryOptions ? JSON.stringify(rulePrimaryOptions) : ''; - - if (printableConfig && ruleSecondaryOptions) { - printableConfig += ', ' + JSON.stringify(ruleSecondaryOptions); - } - - function createCaseDescription(code) { - let text = `\n> rule: ${ruleName}\n`; - - text += `> config: ${printableConfig}\n`; - text += `> code: ${JSON.stringify(code)}\n`; - - return text; - } - - // Process the code through the rule and return - // the PostCSS LazyResult promise - function postcssProcess(code) { - const postcssProcessOptions = {}; - - switch (schema.syntax) { - case 'sass': - postcssProcessOptions.syntax = sassSyntax; - break; - case 'scss': - postcssProcessOptions.syntax = scssSyntax; - break; - case 'less': - postcssProcessOptions.syntax = lessSyntax; - break; - case 'sugarss': - postcssProcessOptions.syntax = sugarss; - break; - } - - const processor = postcss(); - - processor.use(assignDisabledRanges); - - if (schema.preceedingPlugins) { - schema.preceedingPlugins.forEach((plugin) => processor.use(plugin)); - } - - return processor - .use(rule(rulePrimaryOptions, ruleSecondaryOptions)) - .process(code, { postcssProcessOptions, from: undefined }); - } - - // Apply the basic positive checks unless - // explicitly told not to - const passingTestCases = schema.skipBasicChecks - ? schema.accept - : basicChecks.concat(schema.accept); - - if (passingTestCases && passingTestCases.length) { - passingTestCases.forEach((acceptedCase) => { - if (!acceptedCase) { - return; - } - - const assertionDescription = spaceJoin(acceptedCase.description, 'should be accepted'); - const resultPromise = postcssProcess(acceptedCase.code) - .then((postcssResult) => { - const warnings = postcssResult.warnings(); - - return [ - { - expected: 0, - actual: warnings.length, - description: assertionDescription, - }, - ]; - }) - .catch((err) => console.log(err.stack)); // eslint-disable-line no-console - - equalityCheck(resultPromise, { - comparisonCount: 1, - caseDescription: createCaseDescription(acceptedCase.code), - completeAssertionDescription: assertionDescription, - }); - }); - } - - if (schema.reject && schema.reject.length) { - schema.reject.forEach((rejectedCase) => { - let completeAssertionDescription = 'should register one warning'; - let comparisonCount = 1; - - if (rejectedCase.line) { - comparisonCount++; - completeAssertionDescription += ` on line ${rejectedCase.line}`; - } - - if (rejectedCase.column !== undefined) { - comparisonCount++; - completeAssertionDescription += ` on column ${rejectedCase.column}`; - } - - if (rejectedCase.message) { - comparisonCount++; - completeAssertionDescription += ` with message "${rejectedCase.message}"`; - } - - const resultPromise = postcssProcess(rejectedCase.code) - .then((postcssResult) => { - const warnings = postcssResult.warnings(); - const warning = warnings[0]; - - const comparisons = [ - { - expected: 1, - actual: warnings.length, - description: spaceJoin(rejectedCase.description, 'should register one warning'), - }, - ]; - - if (rejectedCase.line) { - comparisons.push({ - expected: rejectedCase.line, - actual: _.get(warning, 'line'), - description: spaceJoin( - rejectedCase.description, - `should warn on line ${rejectedCase.line}`, - ), - }); - } - - if (rejectedCase.column !== undefined) { - comparisons.push({ - expected: rejectedCase.column, - actual: _.get(warning, 'column'), - description: spaceJoin( - rejectedCase.description, - `should warn on column ${rejectedCase.column}`, - ), - }); - } - - if (rejectedCase.message) { - comparisons.push({ - expected: rejectedCase.message, - actual: _.get(warning, 'text'), - description: spaceJoin( - rejectedCase.description, - `should warn with message ${rejectedCase.message}`, - ), - }); - } - - return comparisons; - }) - .catch((err) => console.log(err.stack)); // eslint-disable-line no-console - - equalityCheck(resultPromise, { - comparisonCount, - completeAssertionDescription, - caseDescription: createCaseDescription(rejectedCase.code), - only: rejectedCase.only, - }); - }); - } -} - -function spaceJoin() { - return _.compact(Array.from(arguments)).join(' '); -} - -module.exports = deprecate( - createRuleTester, - 'createRuleTester deprecated. See https://github.com/stylelint/stylelint/issues/4267', -); diff --git a/types/stylelint/index.d.ts b/types/stylelint/index.d.ts index 86a0f129f5..bad4b72a05 100644 --- a/types/stylelint/index.d.ts +++ b/types/stylelint/index.d.ts @@ -207,7 +207,6 @@ declare module 'stylelint' { rules: {[k: string]: any}, formatters: {[k: string]: Function}, createPlugin: Function, - createRuleTester: Function, createLinter: Function, utils: { report: Function,