UNPKG

10.1 kBJavaScriptView Raw
1"use strict";
2
3const _ = require("lodash");
4const assignDisabledRanges = require("../assignDisabledRanges");
5const basicChecks = require("./basicChecks");
6const lessSyntax = require("postcss-less");
7const normalizeRuleSettings = require("../normalizeRuleSettings");
8const postcss = require("postcss");
9const sassSyntax = require("postcss-sass");
10const scssSyntax = require("postcss-scss");
11const sugarss = require("sugarss");
12
13/**
14 * Create a stylelint rule testing function.
15 *
16 * Pass in an `equalityCheck` function. Given some information,
17 * this checker should use Whatever Test Runner to perform
18 * equality checks.
19 *
20 * `equalityCheck` should accept two arguments:
21 * - `processCss` {Promise}: A Promise that resolves with an array of
22 * comparisons that you need to check (documented below).
23 * - `context` {object}: An object that contains additional information
24 * you may need:
25 * - `caseDescription` {string}: A description of the test case as a whole.
26 * Will look like this:
27 * > rule: value-list-comma-space-before
28 * > config: "always-single-line"
29 * > code: "a { background-size: 0 ,0;\n}"
30 * - `comparisonCount` {number}: The number of comparisons that
31 * will need to be performed (e.g. useful for tape).
32 * - `completeAssertionDescription` {string}: While each individual
33 * comparison may have its own description, this is a description
34 * of the whole assertion (e.g. useful for Mocha).
35 * - `only` {boolean}: If `true`, the test runner should only run this
36 * test case (e.g. `test.only` in tape, `describe.only` in Mocha).
37 *
38 * `processCss` is a Promsie that resolves with an array of comparisons.
39 * Each comparison has the following properties:
40 * - `actual` {any}: Some actual value.
41 * - `expected` {any}: Some expected value.
42 * - `description` {string}: A (possibly empty) description of the comparison.
43 *
44 * Within `equalityCheck`, you need to ensure that you:
45 * - Set up the test case.
46 * - When `processCss` resolves, loop through every comparison.
47 * - For each comparison, make an assertion checking that `actual === expected`.
48 *
49 * The `testRule` function that you get has a simple signature:
50 * `testRule(rule, testGroupDescription)`.
51 *
52 * `rule` is just the rule that you are testing (a function).
53 *
54 * `testGroupDescription` is an object fitting the following schema.
55 *
56 * Required properties:
57 * - `ruleName` {string}: The name of the rule. Used in descriptions.
58 * - `config` {any}: The rule's configuration for this test group.
59 * Should match the format you'd use in `.stylelintrc`.
60 * - `accept` {array}: An array of objects describing test cases that
61 * should not violate the rule. Each object has these properties:
62 * - `code` {string}: The source CSS to check.
63 * - `description` {[string]}: An optional description of the case.
64 * - `reject` {array}: An array of objects describing test cases that
65 * should violate the rule once. Each object has these properties:
66 * - `code` {string}: The source CSS to check.
67 * - `message` {string}: The message of the expected violation.
68 * - `line` {[number]}: The expected line number of the violation.
69 * If this is left out, the line won't be checked.
70 * - `column` {[number]}: The expected column number of the violation.
71 * If this is left out, the column won't be checked.
72 * - `description` {[string]}: An optional description of the case.
73 *
74 * Optional properties:
75 * - `syntax` {"css"|"scss"|"less"|"sugarss"}: Defaults to `"css"`.
76 * - `skipBasicChecks` {boolean}: Defaults to `false`. If `true`, a
77 * few rudimentary checks (that should almost always be included)
78 * will not be performed.
79 * - `preceedingPlugins` {array}: An array of PostCSS plugins that
80 * should be run before the CSS is tested.
81 *
82 * @param {function} equalityCheck - Described above
83 * @return {function} testRule - Decsribed above
84 */
85let onlyTest;
86
87function checkCaseForOnly(caseType, testCase) {
88 if (!testCase.only) {
89 return;
90 }
91
92 /* istanbul ignore next */
93 if (onlyTest) {
94 throw new Error("Cannot use `only` on multiple test cases");
95 }
96
97 onlyTest = { case: testCase, type: caseType };
98}
99
100module.exports = function(equalityCheck) {
101 return function(rule, schema) {
102 const alreadyHadOnlyTest = !!onlyTest;
103
104 if (schema.accept) {
105 schema.accept.forEach(_.partial(checkCaseForOnly, "accept"));
106 }
107
108 if (schema.reject) {
109 schema.reject.forEach(_.partial(checkCaseForOnly, "reject"));
110 }
111
112 if (onlyTest) {
113 schema = _.assign(_.omit(schema, ["accept", "reject"]), {
114 skipBasicChecks: true,
115 [onlyTest.type]: [onlyTest.case]
116 });
117 }
118
119 if (!alreadyHadOnlyTest) {
120 process.nextTick(() => {
121 processGroup(rule, schema, equalityCheck);
122 });
123 }
124 };
125};
126
127function processGroup(rule, schema, equalityCheck) {
128 const ruleName = schema.ruleName;
129
130 const ruleOptions = normalizeRuleSettings(schema.config, ruleName);
131 const rulePrimaryOptions = ruleOptions[0];
132 const ruleSecondaryOptions = ruleOptions[1];
133
134 let printableConfig = rulePrimaryOptions
135 ? JSON.stringify(rulePrimaryOptions)
136 : "";
137
138 if (printableConfig && ruleSecondaryOptions) {
139 printableConfig += ", " + JSON.stringify(ruleSecondaryOptions);
140 }
141
142 function createCaseDescription(code) {
143 let text = `\n> rule: ${ruleName}\n`;
144
145 text += `> config: ${printableConfig}\n`;
146 text += `> code: ${JSON.stringify(code)}\n`;
147
148 return text;
149 }
150
151 // Process the code through the rule and return
152 // the PostCSS LazyResult promise
153 function postcssProcess(code) {
154 const postcssProcessOptions = {};
155
156 switch (schema.syntax) {
157 case "sass":
158 postcssProcessOptions.syntax = sassSyntax;
159 break;
160 case "scss":
161 postcssProcessOptions.syntax = scssSyntax;
162 break;
163 case "less":
164 postcssProcessOptions.syntax = lessSyntax;
165 break;
166 case "sugarss":
167 postcssProcessOptions.syntax = sugarss;
168 break;
169 }
170
171 const processor = postcss();
172
173 processor.use(assignDisabledRanges);
174
175 if (schema.preceedingPlugins) {
176 schema.preceedingPlugins.forEach(plugin => processor.use(plugin));
177 }
178
179 return processor
180 .use(rule(rulePrimaryOptions, ruleSecondaryOptions))
181 .process(code, { postcssProcessOptions, from: undefined });
182 }
183
184 // Apply the basic positive checks unless
185 // explicitly told not to
186 const passingTestCases = schema.skipBasicChecks
187 ? schema.accept
188 : basicChecks.concat(schema.accept);
189
190 if (passingTestCases && passingTestCases.length) {
191 passingTestCases.forEach(acceptedCase => {
192 if (!acceptedCase) {
193 return;
194 }
195
196 const assertionDescription = spaceJoin(
197 acceptedCase.description,
198 "should be accepted"
199 );
200 const resultPromise = postcssProcess(acceptedCase.code)
201 .then(postcssResult => {
202 const warnings = postcssResult.warnings();
203
204 return [
205 {
206 expected: 0,
207 actual: warnings.length,
208 description: assertionDescription
209 }
210 ];
211 })
212 .catch(err => console.log(err.stack)); // eslint-disable-line no-console
213
214 equalityCheck(resultPromise, {
215 comparisonCount: 1,
216 caseDescription: createCaseDescription(acceptedCase.code),
217 completeAssertionDescription: assertionDescription
218 });
219 });
220 }
221
222 if (schema.reject && schema.reject.length) {
223 schema.reject.forEach(rejectedCase => {
224 let completeAssertionDescription = "should register one warning";
225 let comparisonCount = 1;
226
227 if (rejectedCase.line) {
228 comparisonCount++;
229 completeAssertionDescription += ` on line ${rejectedCase.line}`;
230 }
231
232 if (rejectedCase.column !== undefined) {
233 comparisonCount++;
234 completeAssertionDescription += ` on column ${rejectedCase.column}`;
235 }
236
237 if (rejectedCase.message) {
238 comparisonCount++;
239 completeAssertionDescription += ` with message "${
240 rejectedCase.message
241 }"`;
242 }
243
244 const resultPromise = postcssProcess(rejectedCase.code)
245 .then(postcssResult => {
246 const warnings = postcssResult.warnings();
247 const warning = warnings[0];
248
249 const comparisons = [
250 {
251 expected: 1,
252 actual: warnings.length,
253 description: spaceJoin(
254 rejectedCase.description,
255 "should register one warning"
256 )
257 }
258 ];
259
260 if (rejectedCase.line) {
261 comparisons.push({
262 expected: rejectedCase.line,
263 actual: _.get(warning, "line"),
264 description: spaceJoin(
265 rejectedCase.description,
266 `should warn on line ${rejectedCase.line}`
267 )
268 });
269 }
270
271 if (rejectedCase.column !== undefined) {
272 comparisons.push({
273 expected: rejectedCase.column,
274 actual: _.get(warning, "column"),
275 description: spaceJoin(
276 rejectedCase.description,
277 `should warn on column ${rejectedCase.column}`
278 )
279 });
280 }
281
282 if (rejectedCase.message) {
283 comparisons.push({
284 expected: rejectedCase.message,
285 actual: _.get(warning, "text"),
286 description: spaceJoin(
287 rejectedCase.description,
288 `should warn with message ${rejectedCase.message}`
289 )
290 });
291 }
292
293 return comparisons;
294 })
295 .catch(err => console.log(err.stack)); // eslint-disable-line no-console
296
297 equalityCheck(resultPromise, {
298 comparisonCount,
299 completeAssertionDescription,
300 caseDescription: createCaseDescription(rejectedCase.code),
301 only: rejectedCase.only
302 });
303 });
304 }
305}
306
307function spaceJoin() {
308 return _.compact(Array.from(arguments)).join(" ");
309}