.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / stylelint / lib / testUtils / createRuleTester.js
1 "use strict";
2
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");
11
12 /**
13  * Create a stylelint rule testing function.
14  *
15  * Pass in an `equalityCheck` function. Given some information,
16  * this checker should use Whatever Test Runner to perform
17  * equality checks.
18  *
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
23  *   you may need:
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).
36  *
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.
42  *
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`.
47  *
48  * The `testRule` function that you get has a simple signature:
49  * `testRule(rule, testGroupDescription)`.
50  *
51  * `rule` is just the rule that you are testing (a function).
52  *
53  * `testGroupDescription` is an object fitting the following schema.
54  *
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.
72  *
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.
80  *
81  * @param {function} equalityCheck - Described above
82  * @return {function} testRule - Decsribed above
83  */
84 let onlyTest;
85
86 function checkCaseForOnly(caseType, testCase) {
87   if (!testCase.only) {
88     return;
89   }
90   /* istanbul ignore next */
91   if (onlyTest) {
92     throw new Error("Cannot use `only` on multiple test cases");
93   }
94   onlyTest = { case: testCase, type: caseType };
95 }
96
97 module.exports = function(equalityCheck) {
98   return function(rule, schema) {
99     const alreadyHadOnlyTest = !!onlyTest;
100     if (schema.accept) {
101       schema.accept.forEach(_.partial(checkCaseForOnly, "accept"));
102     }
103
104     if (schema.reject) {
105       schema.reject.forEach(_.partial(checkCaseForOnly, "reject"));
106     }
107
108     if (onlyTest) {
109       schema = _.assign(_.omit(schema, ["accept", "reject"]), {
110         skipBasicChecks: true,
111         [onlyTest.type]: [onlyTest.case]
112       });
113     }
114
115     if (!alreadyHadOnlyTest) {
116       process.nextTick(() => {
117         processGroup(rule, schema, equalityCheck);
118       });
119     }
120   };
121 };
122
123 function processGroup(rule, schema, equalityCheck) {
124   const ruleName = schema.ruleName;
125
126   const ruleOptions = normalizeRuleSettings(schema.config, ruleName);
127   const rulePrimaryOptions = ruleOptions[0];
128   const ruleSecondaryOptions = ruleOptions[1];
129
130   let printableConfig = rulePrimaryOptions
131     ? JSON.stringify(rulePrimaryOptions)
132     : "";
133   if (printableConfig && ruleSecondaryOptions) {
134     printableConfig += ", " + JSON.stringify(ruleSecondaryOptions);
135   }
136
137   function createCaseDescription(code) {
138     let text = `\n> rule: ${ruleName}\n`;
139     text += `> config: ${printableConfig}\n`;
140     text += `> code: ${JSON.stringify(code)}\n`;
141     return text;
142   }
143
144   // Process the code through the rule and return
145   // the PostCSS LazyResult promise
146   function postcssProcess(code) {
147     const postcssProcessOptions = {};
148
149     switch (schema.syntax) {
150       case "scss":
151         postcssProcessOptions.syntax = scssSyntax;
152         break;
153       case "less":
154         postcssProcessOptions.syntax = lessSyntax;
155         break;
156       case "sugarss":
157         postcssProcessOptions.syntax = sugarss;
158         break;
159     }
160
161     const processor = postcss();
162     processor.use(assignDisabledRanges);
163
164     if (schema.preceedingPlugins) {
165       schema.preceedingPlugins.forEach(plugin => processor.use(plugin));
166     }
167
168     return processor
169       .use(rule(rulePrimaryOptions, ruleSecondaryOptions))
170       .process(code, postcssProcessOptions);
171   }
172
173   // Apply the basic positive checks unless
174   // explicitly told not to
175   const passingTestCases = schema.skipBasicChecks
176     ? schema.accept
177     : basicChecks.concat(schema.accept);
178
179   if (passingTestCases && passingTestCases.length) {
180     passingTestCases.forEach(acceptedCase => {
181       if (!acceptedCase) {
182         return;
183       }
184       const assertionDescription = spaceJoin(
185         acceptedCase.description,
186         "should be accepted"
187       );
188       const resultPromise = postcssProcess(acceptedCase.code)
189         .then(postcssResult => {
190           const warnings = postcssResult.warnings();
191           return [
192             {
193               expected: 0,
194               actual: warnings.length,
195               description: assertionDescription
196             }
197           ];
198         })
199         .catch(err => console.log(err.stack)); // eslint-disable-line no-console
200
201       equalityCheck(resultPromise, {
202         comparisonCount: 1,
203         caseDescription: createCaseDescription(acceptedCase.code),
204         completeAssertionDescription: assertionDescription
205       });
206     });
207   }
208
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) {
214         comparisonCount++;
215         completeAssertionDescription += ` on line ${rejectedCase.line}`;
216       }
217       if (rejectedCase.column !== undefined) {
218         comparisonCount++;
219         completeAssertionDescription += ` on column ${rejectedCase.column}`;
220       }
221       if (rejectedCase.message) {
222         comparisonCount++;
223         completeAssertionDescription += ` with message "${
224           rejectedCase.message
225         }"`;
226       }
227
228       const resultPromise = postcssProcess(rejectedCase.code)
229         .then(postcssResult => {
230           const warnings = postcssResult.warnings();
231           const warning = warnings[0];
232
233           const comparisons = [
234             {
235               expected: 1,
236               actual: warnings.length,
237               description: spaceJoin(
238                 rejectedCase.description,
239                 "should register one warning"
240               )
241             }
242           ];
243
244           if (rejectedCase.line) {
245             comparisons.push({
246               expected: rejectedCase.line,
247               actual: _.get(warning, "line"),
248               description: spaceJoin(
249                 rejectedCase.description,
250                 `should warn on line ${rejectedCase.line}`
251               )
252             });
253           }
254           if (rejectedCase.column !== undefined) {
255             comparisons.push({
256               expected: rejectedCase.column,
257               actual: _.get(warning, "column"),
258               description: spaceJoin(
259                 rejectedCase.description,
260                 `should warn on column ${rejectedCase.column}`
261               )
262             });
263           }
264           if (rejectedCase.message) {
265             comparisons.push({
266               expected: rejectedCase.message,
267               actual: _.get(warning, "text"),
268               description: spaceJoin(
269                 rejectedCase.description,
270                 `should warn with message ${rejectedCase.message}`
271               )
272             });
273           }
274           return comparisons;
275         })
276         .catch(err => console.log(err.stack)); // eslint-disable-line no-console
277
278       equalityCheck(resultPromise, {
279         comparisonCount,
280         completeAssertionDescription,
281         caseDescription: createCaseDescription(rejectedCase.code),
282         only: rejectedCase.only
283       });
284     });
285   }
286 }
287
288 function spaceJoin() {
289   return _.compact(Array.from(arguments)).join(" ");
290 }