less 4.4.2 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Gruntfile.js +16 -17
- package/bin/lessc +35 -40
- package/dist/less.js +118 -185
- package/dist/less.min.js +2 -2
- package/dist/less.min.js.map +1 -1
- package/lib/less/contexts.js +0 -1
- package/lib/less/contexts.js.map +1 -1
- package/lib/less/default-options.js +22 -3
- package/lib/less/default-options.js.map +1 -1
- package/lib/less/less-error.js +2 -1
- package/lib/less/less-error.js.map +1 -1
- package/lib/less/parse-tree.js +58 -1
- package/lib/less/parse-tree.js.map +1 -1
- package/lib/less/parser/parser-input.js +2 -19
- package/lib/less/parser/parser-input.js.map +1 -1
- package/lib/less/parser/parser.js +2 -14
- package/lib/less/parser/parser.js.map +1 -1
- package/lib/less/source-map-output.js +1 -1
- package/lib/less/source-map-output.js.map +1 -1
- package/lib/less/tree/debug-info.js +29 -0
- package/lib/less/tree/debug-info.js.map +1 -1
- package/lib/less-node/lessc-helper.js +8 -7
- package/lib/less-node/lessc-helper.js.map +1 -1
- package/package.json +10 -5
- package/scripts/coverage-lines.js +207 -0
- package/scripts/coverage-report.js +158 -0
- package/scripts/postinstall.js +61 -0
- package/test/browser/common.js +22 -1
- package/test/browser/generator/runner.config.js +20 -24
- package/test/browser/generator/template.js +14 -3
- package/test/browser/runner-browser-options.js +5 -5
- package/test/index.js +293 -98
- package/test/less-test.js +453 -94
- package/test/sourcemaps/comprehensive.json +1 -0
- package/test/sourcemaps/sourcemaps-basepath.json +1 -0
- package/test/sourcemaps/sourcemaps-include-source.json +1 -0
- package/test/sourcemaps/sourcemaps-rootpath.json +1 -0
- package/test/sourcemaps/sourcemaps-url.json +1 -0
- package/test.less +1 -0
- package/lib/less/parser/chunker.js +0 -148
- package/lib/less/parser/chunker.js.map +0 -1
- package/test/browser/runner-legacy-options.js +0 -6
- package/test/browser/runner-legacy-spec.js +0 -3
package/test/less-test.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* jshint latedef: nofunc */
|
|
2
2
|
var semver = require('semver');
|
|
3
3
|
var logger = require('../lib/less/logger').default;
|
|
4
|
+
var { cosmiconfigSync } = require('cosmiconfig');
|
|
5
|
+
var glob = require('glob');
|
|
4
6
|
|
|
5
7
|
var isVerbose = process.env.npm_config_loglevel !== 'concise';
|
|
6
8
|
logger.addListener({
|
|
@@ -18,7 +20,7 @@ logger.addListener({
|
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
module.exports = function() {
|
|
23
|
+
module.exports = function(testFilter) {
|
|
22
24
|
var path = require('path'),
|
|
23
25
|
fs = require('fs'),
|
|
24
26
|
clone = require('copy-anything').copy;
|
|
@@ -29,11 +31,11 @@ module.exports = function() {
|
|
|
29
31
|
|
|
30
32
|
var globals = Object.keys(global);
|
|
31
33
|
|
|
32
|
-
var oneTestOnly = process.argv[2],
|
|
34
|
+
var oneTestOnly = testFilter || process.argv[2],
|
|
33
35
|
isFinished = false;
|
|
34
36
|
|
|
35
37
|
var testFolder = path.dirname(require.resolve('@less/test-data'));
|
|
36
|
-
var lessFolder =
|
|
38
|
+
var lessFolder = testFolder;
|
|
37
39
|
|
|
38
40
|
// Define String.prototype.endsWith if it doesn't exist (in older versions of node)
|
|
39
41
|
// This is required by the testSourceMap function below
|
|
@@ -83,33 +85,202 @@ module.exports = function() {
|
|
|
83
85
|
}
|
|
84
86
|
});
|
|
85
87
|
|
|
86
|
-
function
|
|
88
|
+
function validateSourcemapMappings(sourcemap, lessFile, compiledCSS) {
|
|
89
|
+
// Validate sourcemap mappings using SourceMapConsumer
|
|
90
|
+
var SourceMapConsumer = require('source-map').SourceMapConsumer;
|
|
91
|
+
// sourcemap can be either a string or already parsed object
|
|
92
|
+
var sourceMapObj = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap;
|
|
93
|
+
var consumer = new SourceMapConsumer(sourceMapObj);
|
|
94
|
+
|
|
95
|
+
// Read the LESS source file
|
|
96
|
+
var lessSource = fs.readFileSync(lessFile, 'utf8');
|
|
97
|
+
var lessLines = lessSource.split('\n');
|
|
98
|
+
|
|
99
|
+
// Use the compiled CSS (remove sourcemap annotation for validation)
|
|
100
|
+
var cssSource = compiledCSS.replace(/\/\*# sourceMappingURL=.*\*\/\s*$/, '').trim();
|
|
101
|
+
var cssLines = cssSource.split('\n');
|
|
102
|
+
|
|
103
|
+
var errors = [];
|
|
104
|
+
var validatedMappings = 0;
|
|
105
|
+
|
|
106
|
+
// Validate mappings for each line in the CSS
|
|
107
|
+
for (var cssLine = 1; cssLine <= cssLines.length; cssLine++) {
|
|
108
|
+
var cssLineContent = cssLines[cssLine - 1];
|
|
109
|
+
// Skip empty lines
|
|
110
|
+
if (!cssLineContent.trim()) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check mapping for the start of this CSS line
|
|
115
|
+
var mapping = consumer.originalPositionFor({
|
|
116
|
+
line: cssLine,
|
|
117
|
+
column: 0
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (mapping.source) {
|
|
121
|
+
validatedMappings++;
|
|
122
|
+
|
|
123
|
+
// Verify the source file exists in the sourcemap
|
|
124
|
+
if (!sourceMapObj.sources || sourceMapObj.sources.indexOf(mapping.source) === -1) {
|
|
125
|
+
errors.push('Line ' + cssLine + ': mapped to source "' + mapping.source + '" which is not in sources array');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Verify the line number is valid
|
|
129
|
+
if (mapping.line && mapping.line > 0) {
|
|
130
|
+
// If we can find the source file, validate the line exists
|
|
131
|
+
var sourceIndex = sourceMapObj.sources.indexOf(mapping.source);
|
|
132
|
+
if (sourceIndex >= 0 && sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[sourceIndex] !== undefined && sourceMapObj.sourcesContent[sourceIndex] !== null) {
|
|
133
|
+
var sourceContent = sourceMapObj.sourcesContent[sourceIndex];
|
|
134
|
+
// Ensure sourceContent is a string (it should be, but be defensive)
|
|
135
|
+
if (typeof sourceContent !== 'string') {
|
|
136
|
+
sourceContent = String(sourceContent);
|
|
137
|
+
}
|
|
138
|
+
// Split by newline - handle both \n and \r\n
|
|
139
|
+
var sourceLines = sourceContent.split(/\r?\n/);
|
|
140
|
+
if (mapping.line > sourceLines.length) {
|
|
141
|
+
errors.push('Line ' + cssLine + ': mapped to line ' + mapping.line + ' in "' + mapping.source + '" but source only has ' + sourceLines.length + ' lines');
|
|
142
|
+
}
|
|
143
|
+
} else if (sourceIndex >= 0) {
|
|
144
|
+
// Source content not embedded, try to validate against the actual file if it matches
|
|
145
|
+
// This is a best-effort validation
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate that all sources in the sourcemap are valid
|
|
152
|
+
if (sourceMapObj.sources) {
|
|
153
|
+
sourceMapObj.sources.forEach(function(source, index) {
|
|
154
|
+
if (sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[index]) {
|
|
155
|
+
// Source content is embedded, validate it's not empty
|
|
156
|
+
if (!sourceMapObj.sourcesContent[index].trim()) {
|
|
157
|
+
errors.push('Source "' + source + '" has empty content');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (consumer.destroy && typeof consumer.destroy === 'function') {
|
|
164
|
+
consumer.destroy();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
valid: errors.length === 0,
|
|
169
|
+
errors: errors,
|
|
170
|
+
mappingsValidated: validatedMappings
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function testSourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder, getFilename) {
|
|
87
175
|
if (err) {
|
|
88
176
|
fail('ERROR: ' + (err && err.message));
|
|
89
177
|
return;
|
|
90
178
|
}
|
|
91
179
|
// Check the sourceMappingURL at the bottom of the file
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
sourceMappingSuffix = ' */'
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
180
|
+
// Default expected URL is name + '.css.map', but can be overridden by sourceMapURL option
|
|
181
|
+
var sourceMappingPrefix = '/*# sourceMappingURL=',
|
|
182
|
+
sourceMappingSuffix = ' */';
|
|
183
|
+
var indexOfSourceMappingPrefix = compiledLess.indexOf(sourceMappingPrefix);
|
|
184
|
+
if (indexOfSourceMappingPrefix === -1) {
|
|
185
|
+
fail('ERROR: sourceMappingURL was not found in ' + baseFolder + '/' + name + '.css.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
var startOfSourceMappingValue = indexOfSourceMappingPrefix + sourceMappingPrefix.length,
|
|
190
|
+
indexOfSuffix = compiledLess.indexOf(sourceMappingSuffix, startOfSourceMappingValue),
|
|
191
|
+
actualSourceMapURL = compiledLess.substring(startOfSourceMappingValue, indexOfSuffix === -1 ? compiledLess.length : indexOfSuffix).trim();
|
|
192
|
+
|
|
193
|
+
// For tests with custom sourceMapURL, we just verify it exists and is non-empty
|
|
194
|
+
// The actual value will be validated by comparing the sourcemap JSON
|
|
195
|
+
if (!actualSourceMapURL) {
|
|
196
|
+
fail('ERROR: sourceMappingURL is empty in ' + baseFolder + '/' + name + '.css.');
|
|
197
|
+
return;
|
|
108
198
|
}
|
|
109
199
|
|
|
110
|
-
|
|
200
|
+
// Use getFilename if available (for sourcemap tests with subdirectories)
|
|
201
|
+
var jsonPath;
|
|
202
|
+
if (getFilename && typeof getFilename === 'function') {
|
|
203
|
+
jsonPath = getFilename(name, 'sourcemap', baseFolder);
|
|
204
|
+
} else {
|
|
205
|
+
// Fallback: extract just the filename for sourcemap JSON files
|
|
206
|
+
var jsonFilename = path.basename(name);
|
|
207
|
+
jsonPath = path.join('test/sourcemaps', jsonFilename) + '.json';
|
|
208
|
+
}
|
|
209
|
+
fs.readFile(jsonPath, 'utf8', function (e, expectedSourcemap) {
|
|
111
210
|
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
|
112
|
-
if (
|
|
211
|
+
if (e) {
|
|
212
|
+
fail('ERROR: Could not read expected sourcemap file: ' + jsonPath + ' - ' + e.message);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Apply doReplacements to the expected sourcemap to handle {path} placeholders
|
|
217
|
+
// This normalizes absolute paths that differ between environments
|
|
218
|
+
// For sourcemaps, we need to ensure {path} uses forward slashes to avoid breaking JSON
|
|
219
|
+
// (backslashes in JSON strings need escaping, and sourcemaps should use forward slashes anyway)
|
|
220
|
+
var replacementPath = path.join(path.dirname(path.join(baseFolder, name) + '.less'), '/');
|
|
221
|
+
// Normalize to forward slashes for sourcemap JSON (web-compatible)
|
|
222
|
+
replacementPath = replacementPath.replace(/\\/g, '/');
|
|
223
|
+
// Replace {path} with normalized forward-slash path BEFORE calling doReplacements
|
|
224
|
+
// This ensures the JSON is always valid and uses web-compatible paths
|
|
225
|
+
expectedSourcemap = expectedSourcemap.replace(/\{path\}/g, replacementPath);
|
|
226
|
+
// Also handle other placeholders that might be in the sourcemap (but {path} is already done)
|
|
227
|
+
expectedSourcemap = doReplacements(expectedSourcemap, baseFolder, path.join(baseFolder, name) + '.less');
|
|
228
|
+
|
|
229
|
+
// Normalize paths in sourcemap JSON to use forward slashes (web-compatible)
|
|
230
|
+
// We need to parse the JSON, normalize the file property, then stringify for comparison
|
|
231
|
+
// This avoids breaking escape sequences like \n in the JSON string
|
|
232
|
+
function normalizeSourcemapPaths(sm) {
|
|
233
|
+
try {
|
|
234
|
+
var parsed = typeof sm === 'string' ? JSON.parse(sm) : sm;
|
|
235
|
+
if (parsed.file) {
|
|
236
|
+
parsed.file = parsed.file.replace(/\\/g, '/');
|
|
237
|
+
}
|
|
238
|
+
// Also normalize paths in sources array
|
|
239
|
+
if (parsed.sources && Array.isArray(parsed.sources)) {
|
|
240
|
+
parsed.sources = parsed.sources.map(function(src) {
|
|
241
|
+
return src.replace(/\\/g, '/');
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return JSON.stringify(parsed, null, 0);
|
|
245
|
+
} catch (parseErr) {
|
|
246
|
+
// If parsing fails, return original (shouldn't happen)
|
|
247
|
+
return sm;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
var normalizedSourcemap = normalizeSourcemapPaths(sourcemap);
|
|
252
|
+
var normalizedExpected = normalizeSourcemapPaths(expectedSourcemap);
|
|
253
|
+
|
|
254
|
+
if (normalizedSourcemap === normalizedExpected) {
|
|
255
|
+
// Validate the sourcemap mappings are correct
|
|
256
|
+
// Find the actual LESS file - it might be in a subdirectory
|
|
257
|
+
var nameParts = name.split('/');
|
|
258
|
+
var lessFileName = nameParts[nameParts.length - 1];
|
|
259
|
+
var lessFileDir = nameParts.length > 1 ? nameParts.slice(0, -1).join('/') : '';
|
|
260
|
+
var lessFile = path.join(lessFolder, lessFileDir, lessFileName) + '.less';
|
|
261
|
+
|
|
262
|
+
// Only validate if the LESS file exists
|
|
263
|
+
if (fs.existsSync(lessFile)) {
|
|
264
|
+
try {
|
|
265
|
+
// Parse the sourcemap once for validation (avoid re-parsing)
|
|
266
|
+
// Use the original sourcemap string, not the normalized one
|
|
267
|
+
var sourceMapObjForValidation = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap;
|
|
268
|
+
var validation = validateSourcemapMappings(sourceMapObjForValidation, lessFile, compiledLess);
|
|
269
|
+
if (!validation.valid) {
|
|
270
|
+
fail('ERROR: Sourcemap validation failed:\n' + validation.errors.join('\n'));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (isVerbose && validation.mappingsValidated > 0) {
|
|
274
|
+
process.stdout.write(' (validated ' + validation.mappingsValidated + ' mappings)');
|
|
275
|
+
}
|
|
276
|
+
} catch (validationErr) {
|
|
277
|
+
if (isVerbose) {
|
|
278
|
+
process.stdout.write(' (validation error: ' + validationErr.message + ')');
|
|
279
|
+
}
|
|
280
|
+
// Don't fail the test if validation has an error, just log it
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
113
284
|
ok('OK');
|
|
114
285
|
} else if (err) {
|
|
115
286
|
fail('ERROR: ' + (err && err.message));
|
|
@@ -118,7 +289,7 @@ module.exports = function() {
|
|
|
118
289
|
process.stdout.write(err.stack + '\n');
|
|
119
290
|
}
|
|
120
291
|
} else {
|
|
121
|
-
difference('FAIL',
|
|
292
|
+
difference('FAIL', normalizedExpected, normalizedSourcemap);
|
|
122
293
|
}
|
|
123
294
|
});
|
|
124
295
|
}
|
|
@@ -281,7 +452,7 @@ module.exports = function() {
|
|
|
281
452
|
return new less.tree.Anonymous('file');
|
|
282
453
|
});
|
|
283
454
|
var expected = '@charset "utf-8";\n';
|
|
284
|
-
toCSS({}, path.join(lessFolder, 'root-registry', 'root.less'), function(error, output) {
|
|
455
|
+
toCSS({}, path.join(lessFolder, 'tests-config', 'root-registry', 'root.less'), function(error, output) {
|
|
285
456
|
if (error) {
|
|
286
457
|
return fail('ERROR: ' + error);
|
|
287
458
|
}
|
|
@@ -294,9 +465,42 @@ module.exports = function() {
|
|
|
294
465
|
|
|
295
466
|
function globalReplacements(input, directory, filename) {
|
|
296
467
|
var path = require('path');
|
|
297
|
-
var p = filename ? path.join(path.dirname(filename), '/') : directory
|
|
298
|
-
|
|
299
|
-
|
|
468
|
+
var p = filename ? path.join(path.dirname(filename), '/') : directory;
|
|
469
|
+
|
|
470
|
+
// For debug tests in subdirectories (comments/, mediaquery/, all/),
|
|
471
|
+
// the import/ directory and main linenumbers.less file are at the parent debug/ level, not in the subdirectory
|
|
472
|
+
var isDebugSubdirectory = false;
|
|
473
|
+
var debugParentPath = null;
|
|
474
|
+
|
|
475
|
+
if (directory) {
|
|
476
|
+
// Normalize directory path separators for matching
|
|
477
|
+
var normalizedDir = directory.replace(/\\/g, '/');
|
|
478
|
+
// Check if we're in a debug subdirectory
|
|
479
|
+
if (normalizedDir.includes('/debug/') && (normalizedDir.includes('/comments/') || normalizedDir.includes('/mediaquery/') || normalizedDir.includes('/all/'))) {
|
|
480
|
+
isDebugSubdirectory = true;
|
|
481
|
+
// Extract the debug/ directory path (parent of the subdirectory)
|
|
482
|
+
// Match everything up to and including /debug/ (works with both absolute and relative paths)
|
|
483
|
+
var debugMatch = normalizedDir.match(/(.+\/debug)\//);
|
|
484
|
+
if (debugMatch) {
|
|
485
|
+
debugParentPath = debugMatch[1];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (isDebugSubdirectory && debugParentPath) {
|
|
491
|
+
// For {path} placeholder, use the parent debug/ directory
|
|
492
|
+
// Convert back to native path format
|
|
493
|
+
p = debugParentPath.replace(/\//g, path.sep) + path.sep;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
var pathimport;
|
|
497
|
+
if (isDebugSubdirectory && debugParentPath) {
|
|
498
|
+
pathimport = path.join(debugParentPath.replace(/\//g, path.sep), 'import') + path.sep;
|
|
499
|
+
} else {
|
|
500
|
+
pathimport = path.join(directory + 'import/');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
var pathesc = p.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }),
|
|
300
504
|
pathimportesc = pathimport.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); });
|
|
301
505
|
|
|
302
506
|
return input.replace(/\{path\}/g, p)
|
|
@@ -340,7 +544,18 @@ module.exports = function() {
|
|
|
340
544
|
}
|
|
341
545
|
|
|
342
546
|
function runTestSet(options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) {
|
|
343
|
-
|
|
547
|
+
// Handle case where first parameter is glob patterns (no options object)
|
|
548
|
+
if (Array.isArray(options)) {
|
|
549
|
+
// First parameter is glob patterns, no options object
|
|
550
|
+
foldername = options;
|
|
551
|
+
options = {};
|
|
552
|
+
} else if (typeof options === 'string') {
|
|
553
|
+
// First parameter is foldername (no options object)
|
|
554
|
+
foldername = options;
|
|
555
|
+
options = {};
|
|
556
|
+
} else {
|
|
557
|
+
options = options ? clone(options) : {};
|
|
558
|
+
}
|
|
344
559
|
runTestSetInternal(lessFolder, options, foldername, verifyFunction, nameModifier, doReplacements, getFilename);
|
|
345
560
|
}
|
|
346
561
|
|
|
@@ -357,41 +572,125 @@ module.exports = function() {
|
|
|
357
572
|
doReplacements = globalReplacements;
|
|
358
573
|
}
|
|
359
574
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
575
|
+
// Handle glob patterns with exclusions
|
|
576
|
+
if (Array.isArray(foldername)) {
|
|
577
|
+
var patterns = foldername;
|
|
578
|
+
var includePatterns = [];
|
|
579
|
+
var excludePatterns = [];
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
patterns.forEach(function(pattern) {
|
|
583
|
+
if (pattern.startsWith('!')) {
|
|
584
|
+
excludePatterns.push(pattern.substring(1));
|
|
585
|
+
} else {
|
|
586
|
+
includePatterns.push(pattern);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// Use glob to find all matching files, excluding the excluded patterns
|
|
591
|
+
var allFiles = [];
|
|
592
|
+
includePatterns.forEach(function(pattern) {
|
|
593
|
+
var files = glob.sync(pattern, {
|
|
594
|
+
cwd: baseFolder,
|
|
595
|
+
absolute: true,
|
|
596
|
+
ignore: excludePatterns
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
allFiles = allFiles.concat(files);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Note: needle mocking is set up globally in index.js
|
|
603
|
+
|
|
604
|
+
// Process each .less file found
|
|
605
|
+
allFiles.forEach(function(filePath) {
|
|
606
|
+
if (/\.less$/.test(filePath)) {
|
|
607
|
+
var file = path.basename(filePath);
|
|
608
|
+
// For glob patterns, we need to construct the relative path differently
|
|
609
|
+
// The filePath is absolute, so we need to get the path relative to the test-data directory
|
|
610
|
+
var relativePath = path.relative(baseFolder, path.dirname(filePath)) + '/';
|
|
611
|
+
|
|
612
|
+
// Only process files that have corresponding .css files (these are the actual tests)
|
|
613
|
+
var cssPath = path.join(path.dirname(filePath), path.basename(file, '.less') + '.css');
|
|
614
|
+
if (fs.existsSync(cssPath)) {
|
|
615
|
+
// Process this file using the existing logic
|
|
616
|
+
processFileWithInfo({
|
|
617
|
+
file: file,
|
|
618
|
+
fullPath: filePath,
|
|
619
|
+
relativePath: relativePath
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
363
625
|
|
|
364
|
-
|
|
365
|
-
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
366
628
|
|
|
367
|
-
|
|
629
|
+
function processFileWithInfo(fileInfo) {
|
|
630
|
+
var file = fileInfo.file;
|
|
631
|
+
var fullPath = fileInfo.fullPath;
|
|
632
|
+
var relativePath = fileInfo.relativePath;
|
|
633
|
+
|
|
634
|
+
// Load config for this specific file using cosmiconfig
|
|
635
|
+
var configResult = cosmiconfigSync('styles').search(path.dirname(fullPath));
|
|
636
|
+
|
|
637
|
+
// Deep clone the original options to prevent Less from modifying shared objects
|
|
638
|
+
var options = JSON.parse(JSON.stringify(originalOptions || {}));
|
|
639
|
+
|
|
640
|
+
if (configResult && configResult.config && configResult.config.language && configResult.config.language.less) {
|
|
641
|
+
// Deep clone and merge the language.less settings with the original options
|
|
642
|
+
var lessConfig = JSON.parse(JSON.stringify(configResult.config.language.less));
|
|
643
|
+
Object.keys(lessConfig).forEach(function(key) {
|
|
644
|
+
options[key] = lessConfig[key];
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Merge any lessOptions from the testMap (for dynamic options like getVars functions)
|
|
649
|
+
if (originalOptions && originalOptions.lessOptions) {
|
|
650
|
+
Object.keys(originalOptions.lessOptions).forEach(function(key) {
|
|
651
|
+
var value = originalOptions.lessOptions[key];
|
|
652
|
+
if (typeof value === 'function') {
|
|
653
|
+
// For functions, call them with the file path
|
|
654
|
+
var result = value(fullPath);
|
|
655
|
+
options[key] = result;
|
|
656
|
+
} else {
|
|
657
|
+
// For static values, use them directly
|
|
658
|
+
options[key] = value;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
368
662
|
|
|
369
|
-
|
|
663
|
+
// Don't pass stylize to less.render as it's not a valid option
|
|
370
664
|
|
|
371
|
-
var name = getBasename(file);
|
|
665
|
+
var name = getBasename(file, relativePath);
|
|
666
|
+
|
|
372
667
|
|
|
373
|
-
if (oneTestOnly &&
|
|
668
|
+
if (oneTestOnly && typeof oneTestOnly === 'string' && !name.includes(oneTestOnly)) {
|
|
374
669
|
return;
|
|
375
670
|
}
|
|
376
671
|
|
|
377
672
|
totalTests++;
|
|
378
673
|
|
|
379
674
|
if (options.sourceMap && !options.sourceMap.sourceMapFileInline) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
675
|
+
// Set test infrastructure defaults only if not already set by styles.config.cjs
|
|
676
|
+
// Less.js core (parse-tree.js) will handle normalization of:
|
|
677
|
+
// - sourceMapBasepath (defaults to input file's directory)
|
|
678
|
+
// - sourceMapInputFilename (defaults to options.filename)
|
|
679
|
+
// - sourceMapFilename (derived from sourceMapOutputFilename or input filename)
|
|
680
|
+
// - sourceMapOutputFilename (derived from input filename if not set)
|
|
681
|
+
if (!options.sourceMap.sourceMapOutputFilename) {
|
|
682
|
+
// Needed for sourcemap file name in JSON output
|
|
683
|
+
options.sourceMap.sourceMapOutputFilename = name + '.css';
|
|
684
|
+
}
|
|
685
|
+
if (!options.sourceMap.sourceMapRootpath) {
|
|
686
|
+
// Test-specific default for consistent test output paths
|
|
687
|
+
options.sourceMap.sourceMapRootpath = 'testweb/';
|
|
688
|
+
}
|
|
390
689
|
}
|
|
391
690
|
|
|
392
691
|
options.getVars = function(file) {
|
|
393
692
|
try {
|
|
394
|
-
return JSON.parse(fs.readFileSync(getFilename(getBasename(file), 'vars', baseFolder), 'utf8'));
|
|
693
|
+
return JSON.parse(fs.readFileSync(getFilename(getBasename(file, relativePath), 'vars', baseFolder), 'utf8'));
|
|
395
694
|
}
|
|
396
695
|
catch (e) {
|
|
397
696
|
return {};
|
|
@@ -400,7 +699,7 @@ module.exports = function() {
|
|
|
400
699
|
|
|
401
700
|
var doubleCallCheck = false;
|
|
402
701
|
queue(function() {
|
|
403
|
-
toCSS(options,
|
|
702
|
+
toCSS(options, fullPath, function (err, result) {
|
|
404
703
|
|
|
405
704
|
if (doubleCallCheck) {
|
|
406
705
|
totalTests++;
|
|
@@ -416,7 +715,7 @@ module.exports = function() {
|
|
|
416
715
|
*/
|
|
417
716
|
if (verifyFunction) {
|
|
418
717
|
var verificationResult = verifyFunction(
|
|
419
|
-
name, err, result && result.css, doReplacements, result && result.map, baseFolder, result && result.imports
|
|
718
|
+
name, err, result && result.css, doReplacements, result && result.map, baseFolder, result && result.imports, getFilename
|
|
420
719
|
);
|
|
421
720
|
release();
|
|
422
721
|
return verificationResult;
|
|
@@ -439,31 +738,90 @@ module.exports = function() {
|
|
|
439
738
|
var css_name = name;
|
|
440
739
|
if (nameModifier) { css_name = nameModifier(name); }
|
|
441
740
|
|
|
442
|
-
|
|
443
|
-
|
|
741
|
+
// Check if we're using the new co-located structure (tests-unit/ or tests-config/) or the old separated structure
|
|
742
|
+
var cssPath;
|
|
743
|
+
if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) {
|
|
744
|
+
// New co-located structure: CSS file is in the same directory as LESS file
|
|
745
|
+
cssPath = path.join(path.dirname(fullPath), path.basename(file, '.less') + '.css');
|
|
746
|
+
} else {
|
|
747
|
+
// Old separated structure: CSS file is in separate css/ folder
|
|
748
|
+
// Windows compatibility: css_name may already contain path separators
|
|
749
|
+
// Use path.join with empty string to let path.join handle normalization
|
|
750
|
+
cssPath = path.join(testFolder, css_name) + '.css';
|
|
751
|
+
}
|
|
444
752
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
753
|
+
// For the new structure, we need to handle replacements differently
|
|
754
|
+
var replacementPath;
|
|
755
|
+
if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) {
|
|
756
|
+
replacementPath = path.dirname(fullPath);
|
|
757
|
+
// Ensure replacementPath ends with a path separator for consistent matching
|
|
758
|
+
if (!replacementPath.endsWith(path.sep)) {
|
|
759
|
+
replacementPath += path.sep;
|
|
449
760
|
}
|
|
450
|
-
|
|
451
|
-
|
|
761
|
+
} else {
|
|
762
|
+
replacementPath = path.join(baseFolder, relativePath);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
var testName = fullPath.replace(/\.less$/, '');
|
|
766
|
+
process.stdout.write('- ' + testName + ': ');
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
var css = fs.readFileSync(cssPath, 'utf8');
|
|
770
|
+
css = css && doReplacements(css, replacementPath);
|
|
771
|
+
if (result.css === css) { ok('OK'); }
|
|
772
|
+
else {
|
|
773
|
+
difference('FAIL', css, result.css);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
release();
|
|
452
777
|
});
|
|
453
778
|
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function getBasename(file, relativePath) {
|
|
782
|
+
var basePath = relativePath || foldername;
|
|
783
|
+
// Ensure basePath ends with a slash for proper path construction
|
|
784
|
+
if (basePath.charAt(basePath.length - 1) !== '/') {
|
|
785
|
+
basePath = basePath + '/';
|
|
786
|
+
}
|
|
787
|
+
return basePath + path.basename(file, '.less');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
// This function is only called for non-glob patterns now
|
|
792
|
+
// For glob patterns, we use the glob library in the calling code
|
|
793
|
+
var dirPath = path.join(baseFolder, foldername);
|
|
794
|
+
var items = fs.readdirSync(dirPath);
|
|
795
|
+
|
|
796
|
+
items.forEach(function(item) {
|
|
797
|
+
if (/\.less$/.test(item)) {
|
|
798
|
+
processFileWithInfo({
|
|
799
|
+
file: item,
|
|
800
|
+
fullPath: path.join(dirPath, item),
|
|
801
|
+
relativePath: foldername
|
|
802
|
+
});
|
|
803
|
+
}
|
|
454
804
|
});
|
|
455
805
|
}
|
|
456
806
|
|
|
457
807
|
function diff(left, right) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
808
|
+
// Configure chalk to always show colors
|
|
809
|
+
var chalk = require('chalk');
|
|
810
|
+
chalk.level = 3; // Force colors on
|
|
811
|
+
|
|
812
|
+
// Use jest-diff for much clearer output like Vitest
|
|
813
|
+
var diffResult = require('jest-diff').diffStringsUnified(left || '', right || '', {
|
|
814
|
+
expand: false,
|
|
815
|
+
includeChangeCounts: true,
|
|
816
|
+
contextLines: 1,
|
|
817
|
+
aColor: chalk.red,
|
|
818
|
+
bColor: chalk.green,
|
|
819
|
+
changeColor: chalk.inverse,
|
|
820
|
+
commonColor: chalk.dim
|
|
465
821
|
});
|
|
466
|
-
|
|
822
|
+
|
|
823
|
+
// jest-diff returns a string with ANSI colors, so we can output it directly
|
|
824
|
+
process.stdout.write(diffResult + '\n');
|
|
467
825
|
}
|
|
468
826
|
|
|
469
827
|
function fail(msg) {
|
|
@@ -476,6 +834,9 @@ module.exports = function() {
|
|
|
476
834
|
process.stdout.write(stylize(msg, 'yellow') + '\n');
|
|
477
835
|
failedTests++;
|
|
478
836
|
|
|
837
|
+
// Only show the diff, not the full text
|
|
838
|
+
process.stdout.write(stylize('Diff:', 'yellow') + '\n');
|
|
839
|
+
|
|
479
840
|
diff(left || '', right || '');
|
|
480
841
|
endTest();
|
|
481
842
|
}
|
|
@@ -528,27 +889,41 @@ module.exports = function() {
|
|
|
528
889
|
* @param {Function} callback
|
|
529
890
|
*/
|
|
530
891
|
function toCSS(options, filePath, callback) {
|
|
531
|
-
|
|
892
|
+
// Deep clone options to prevent modifying the original, but preserve functions
|
|
893
|
+
var originalOptions = options || {};
|
|
894
|
+
options = JSON.parse(JSON.stringify(originalOptions));
|
|
895
|
+
|
|
896
|
+
// Restore functions that were lost in JSON serialization
|
|
897
|
+
if (originalOptions.getVars) {
|
|
898
|
+
options.getVars = originalOptions.getVars;
|
|
899
|
+
}
|
|
532
900
|
var str = fs.readFileSync(filePath, 'utf8'), addPath = path.dirname(filePath);
|
|
901
|
+
|
|
902
|
+
// Initialize paths array if it doesn't exist
|
|
533
903
|
if (typeof options.paths !== 'string') {
|
|
534
904
|
options.paths = options.paths || [];
|
|
535
|
-
if (!contains(options.paths, addPath)) {
|
|
536
|
-
options.paths.push(addPath);
|
|
537
|
-
}
|
|
538
905
|
} else {
|
|
539
|
-
options.paths = [options.paths]
|
|
906
|
+
options.paths = [options.paths];
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Add the current directory to paths if not already present
|
|
910
|
+
if (!contains(options.paths, addPath)) {
|
|
911
|
+
options.paths.push(addPath);
|
|
540
912
|
}
|
|
913
|
+
|
|
914
|
+
// Resolve all paths relative to the test file's directory
|
|
541
915
|
options.paths = options.paths.map(searchPath => {
|
|
542
|
-
|
|
916
|
+
if (path.isAbsolute(searchPath)) {
|
|
917
|
+
return searchPath;
|
|
918
|
+
}
|
|
919
|
+
// Resolve relative to the test file's directory
|
|
920
|
+
return path.resolve(path.dirname(filePath), searchPath);
|
|
543
921
|
})
|
|
922
|
+
|
|
544
923
|
options.filename = path.resolve(process.cwd(), filePath);
|
|
545
924
|
options.optimization = options.optimization || 0;
|
|
546
925
|
|
|
547
|
-
|
|
548
|
-
options.globalVars = options.getVars(filePath);
|
|
549
|
-
} else if (options.modifyVars) {
|
|
550
|
-
options.modifyVars = options.getVars(filePath);
|
|
551
|
-
}
|
|
926
|
+
// Note: globalVars and modifyVars are now handled via styles.config.cjs or lessOptions
|
|
552
927
|
if (options.plugin) {
|
|
553
928
|
var Plugin = require(path.resolve(process.cwd(), options.plugin));
|
|
554
929
|
options.plugins = [Plugin];
|
|
@@ -571,22 +946,7 @@ module.exports = function() {
|
|
|
571
946
|
ok(stylize('OK\n', 'green'));
|
|
572
947
|
}
|
|
573
948
|
|
|
574
|
-
|
|
575
|
-
return (name, err, css, doReplacements, sourcemap, baseFolder) => {
|
|
576
|
-
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
|
577
|
-
if (err) {
|
|
578
|
-
fail('FAIL: ' + (err && err.message));
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
const expected = 'h1 {\n color: red;\n}\n';
|
|
582
|
-
if (css !== expected) {
|
|
583
|
-
difference('FAIL', expected, css);
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
nockScope.done();
|
|
587
|
-
ok('OK');
|
|
588
|
-
};
|
|
589
|
-
}
|
|
949
|
+
// HTTP redirect testing is now handled directly in test/index.js
|
|
590
950
|
|
|
591
951
|
function testDisablePluginRule() {
|
|
592
952
|
less.render(
|
|
@@ -615,7 +975,6 @@ module.exports = function() {
|
|
|
615
975
|
testSourcemapWithoutUrlAnnotation: testSourcemapWithoutUrlAnnotation,
|
|
616
976
|
testSourcemapWithVariableInSelector: testSourcemapWithVariableInSelector,
|
|
617
977
|
testImports: testImports,
|
|
618
|
-
testImportRedirect: testImportRedirect,
|
|
619
978
|
testEmptySourcemap: testEmptySourcemap,
|
|
620
979
|
testNoOptions: testNoOptions,
|
|
621
980
|
testDisablePluginRule: testDisablePluginRule,
|