3 const _ = require("lodash");
4 const assignDisabledRanges = require("../assignDisabledRanges");
5 const basicChecks = require("./basicChecks");
6 const lessSyntax = require("postcss-less");
7 const normalizeRuleSettings = require("../normalizeRuleSettings");
8 const postcss = require("postcss");
9 const scssSyntax = require("postcss-scss");
10 const sugarss = require("sugarss");
13 * Create a stylelint rule testing function.
15 * Pass in an `equalityCheck` function. Given some information,
16 * this checker should use Whatever Test Runner to perform
19 * `equalityCheck` should accept two arguments:
20 * - `processCss` {Promise}: A Promise that resolves with an array of
21 * comparisons that you need to check (documented below).
22 * - `context` {object}: An object that contains additional information
24 * - `caseDescription` {string}: A description of the test case as a whole.
25 * Will look like this:
26 * > rule: value-list-comma-space-before
27 * > config: "always-single-line"
28 * > code: "a { background-size: 0 ,0;\n}"
29 * - `comparisonCount` {number}: The number of comparisons that
30 * will need to be performed (e.g. useful for tape).
31 * - `completeAssertionDescription` {string}: While each individual
32 * comparison may have its own description, this is a description
33 * of the whole assertion (e.g. useful for Mocha).
34 * - `only` {boolean}: If `true`, the test runner should only run this
35 * test case (e.g. `test.only` in tape, `describe.only` in Mocha).
37 * `processCss` is a Promsie that resolves with an array of comparisons.
38 * Each comparison has the following properties:
39 * - `actual` {any}: Some actual value.
40 * - `expected` {any}: Some expected value.
41 * - `description` {string}: A (possibly empty) description of the comparison.
43 * Within `equalityCheck`, you need to ensure that you:
44 * - Set up the test case.
45 * - When `processCss` resolves, loop through every comparison.
46 * - For each comparison, make an assertion checking that `actual === expected`.
48 * The `testRule` function that you get has a simple signature:
49 * `testRule(rule, testGroupDescription)`.
51 * `rule` is just the rule that you are testing (a function).
53 * `testGroupDescription` is an object fitting the following schema.
55 * Required properties:
56 * - `ruleName` {string}: The name of the rule. Used in descriptions.
57 * - `config` {any}: The rule's configuration for this test group.
58 * Should match the format you'd use in `.stylelintrc`.
59 * - `accept` {array}: An array of objects describing test cases that
60 * should not violate the rule. Each object has these properties:
61 * - `code` {string}: The source CSS to check.
62 * - `description` {[string]}: An optional description of the case.
63 * - `reject` {array}: An array of objects describing test cases that
64 * should violate the rule once. Each object has these properties:
65 * - `code` {string}: The source CSS to check.
66 * - `message` {string}: The message of the expected violation.
67 * - `line` {[number]}: The expected line number of the violation.
68 * If this is left out, the line won't be checked.
69 * - `column` {[number]}: The expected column number of the violation.
70 * If this is left out, the column won't be checked.
71 * - `description` {[string]}: An optional description of the case.
73 * Optional properties:
74 * - `syntax` {"css"|"scss"|"less"|"sugarss"}: Defaults to `"css"`.
75 * - `skipBasicChecks` {boolean}: Defaults to `false`. If `true`, a
76 * few rudimentary checks (that should almost always be included)
77 * will not be performed.
78 * - `preceedingPlugins` {array}: An array of PostCSS plugins that
79 * should be run before the CSS is tested.
81 * @param {function} equalityCheck - Described above
82 * @return {function} testRule - Decsribed above
86 function checkCaseForOnly(caseType, testCase) {
90 /* istanbul ignore next */
92 throw new Error("Cannot use `only` on multiple test cases");
94 onlyTest = { case: testCase, type: caseType };
97 module.exports = function(equalityCheck) {
98 return function(rule, schema) {
99 const alreadyHadOnlyTest = !!onlyTest;
101 schema.accept.forEach(_.partial(checkCaseForOnly, "accept"));
105 schema.reject.forEach(_.partial(checkCaseForOnly, "reject"));
109 schema = _.assign(_.omit(schema, ["accept", "reject"]), {
110 skipBasicChecks: true,
111 [onlyTest.type]: [onlyTest.case]
115 if (!alreadyHadOnlyTest) {
116 process.nextTick(() => {
117 processGroup(rule, schema, equalityCheck);
123 function processGroup(rule, schema, equalityCheck) {
124 const ruleName = schema.ruleName;
126 const ruleOptions = normalizeRuleSettings(schema.config, ruleName);
127 const rulePrimaryOptions = ruleOptions[0];
128 const ruleSecondaryOptions = ruleOptions[1];
130 let printableConfig = rulePrimaryOptions
131 ? JSON.stringify(rulePrimaryOptions)
133 if (printableConfig && ruleSecondaryOptions) {
134 printableConfig += ", " + JSON.stringify(ruleSecondaryOptions);
137 function createCaseDescription(code) {
138 let text = `\n> rule: ${ruleName}\n`;
139 text += `> config: ${printableConfig}\n`;
140 text += `> code: ${JSON.stringify(code)}\n`;
144 // Process the code through the rule and return
145 // the PostCSS LazyResult promise
146 function postcssProcess(code) {
147 const postcssProcessOptions = {};
149 switch (schema.syntax) {
151 postcssProcessOptions.syntax = scssSyntax;
154 postcssProcessOptions.syntax = lessSyntax;
157 postcssProcessOptions.syntax = sugarss;
161 const processor = postcss();
162 processor.use(assignDisabledRanges);
164 if (schema.preceedingPlugins) {
165 schema.preceedingPlugins.forEach(plugin => processor.use(plugin));
169 .use(rule(rulePrimaryOptions, ruleSecondaryOptions))
170 .process(code, postcssProcessOptions);
173 // Apply the basic positive checks unless
174 // explicitly told not to
175 const passingTestCases = schema.skipBasicChecks
177 : basicChecks.concat(schema.accept);
179 if (passingTestCases && passingTestCases.length) {
180 passingTestCases.forEach(acceptedCase => {
184 const assertionDescription = spaceJoin(
185 acceptedCase.description,
188 const resultPromise = postcssProcess(acceptedCase.code)
189 .then(postcssResult => {
190 const warnings = postcssResult.warnings();
194 actual: warnings.length,
195 description: assertionDescription
199 .catch(err => console.log(err.stack)); // eslint-disable-line no-console
201 equalityCheck(resultPromise, {
203 caseDescription: createCaseDescription(acceptedCase.code),
204 completeAssertionDescription: assertionDescription
209 if (schema.reject && schema.reject.length) {
210 schema.reject.forEach(rejectedCase => {
211 let completeAssertionDescription = "should register one warning";
212 let comparisonCount = 1;
213 if (rejectedCase.line) {
215 completeAssertionDescription += ` on line ${rejectedCase.line}`;
217 if (rejectedCase.column !== undefined) {
219 completeAssertionDescription += ` on column ${rejectedCase.column}`;
221 if (rejectedCase.message) {
223 completeAssertionDescription += ` with message "${
228 const resultPromise = postcssProcess(rejectedCase.code)
229 .then(postcssResult => {
230 const warnings = postcssResult.warnings();
231 const warning = warnings[0];
233 const comparisons = [
236 actual: warnings.length,
237 description: spaceJoin(
238 rejectedCase.description,
239 "should register one warning"
244 if (rejectedCase.line) {
246 expected: rejectedCase.line,
247 actual: _.get(warning, "line"),
248 description: spaceJoin(
249 rejectedCase.description,
250 `should warn on line ${rejectedCase.line}`
254 if (rejectedCase.column !== undefined) {
256 expected: rejectedCase.column,
257 actual: _.get(warning, "column"),
258 description: spaceJoin(
259 rejectedCase.description,
260 `should warn on column ${rejectedCase.column}`
264 if (rejectedCase.message) {
266 expected: rejectedCase.message,
267 actual: _.get(warning, "text"),
268 description: spaceJoin(
269 rejectedCase.description,
270 `should warn with message ${rejectedCase.message}`
276 .catch(err => console.log(err.stack)); // eslint-disable-line no-console
278 equalityCheck(resultPromise, {
280 completeAssertionDescription,
281 caseDescription: createCaseDescription(rejectedCase.code),
282 only: rejectedCase.only
288 function spaceJoin() {
289 return _.compact(Array.from(arguments)).join(" ");