mirror of
https://github.com/less/less.js.git
synced 2026-01-08 23:28:04 -05:00
[Needs reviews!] Mega test refactor (#4378)
* Migrate Less to use valid CSS * Re-organize test structure * Lots of test refactoring * More test updates * Add restructured tests * WIP Signed-off-by: Matthew Dean <matthew-dean@users.noreply.github.com> * More fixes to tests * More test fixes * All tests passing * WIP fix tests * Finished fixing browser tests * Improve test coverage * Add debug tests * Add back debug tests to show equal test coverage * Fix browser tests * More test coverage * Fix sourcemap absolute path for CI * More source map normalization for Windows * Fix source map normalization * Another attempted fix for Windows --------- Signed-off-by: Matthew Dean <matthew-dean@users.noreply.github.com>
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -12,3 +12,8 @@
|
||||
node_modules
|
||||
!package-lock.json
|
||||
npm-debug.log
|
||||
|
||||
# Coverage
|
||||
.nyc_output
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
@@ -4,7 +4,7 @@ var resolve = require('resolve');
|
||||
var path = require('path');
|
||||
|
||||
var testFolder = path.relative(process.cwd(), path.dirname(resolve.sync('@less/test-data')));
|
||||
var lessFolder = path.join(testFolder, 'less');
|
||||
var lessFolder = testFolder;
|
||||
|
||||
module.exports = function(grunt) {
|
||||
grunt.option("stack", true);
|
||||
@@ -85,8 +85,7 @@ module.exports = function(grunt) {
|
||||
"relative-urls",
|
||||
"rewrite-urls",
|
||||
"browser",
|
||||
"no-js-errors",
|
||||
"legacy"
|
||||
"no-js-errors"
|
||||
];
|
||||
|
||||
function makeJob(testName) {
|
||||
@@ -214,7 +213,7 @@ module.exports = function(grunt) {
|
||||
command: "node build/rollup.js --browser --out=./tmp/browser/less.min.js"
|
||||
},
|
||||
test: {
|
||||
command: 'ts-node test/test-es6.ts && node test/index.js'
|
||||
command: 'npx ts-node test/test-es6.ts && node test/index.js'
|
||||
},
|
||||
generatebrowser: {
|
||||
command: 'node test/browser/generator/generate.js'
|
||||
@@ -230,35 +229,35 @@ module.exports = function(grunt) {
|
||||
command: [
|
||||
// @TODO: make this more thorough
|
||||
// CURRENT OPTIONS
|
||||
`node bin/lessc --ie-compat ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --ie-compat ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`,
|
||||
// --math
|
||||
`node bin/lessc --math=always ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --math=parens-division ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --math=parens ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --math=strict ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --math=strict-legacy ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --math=always ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --math=parens-division ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --math=parens ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --math=strict ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --math=strict-legacy ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`,
|
||||
|
||||
// DEPRECATED OPTIONS
|
||||
// --strict-math
|
||||
`node bin/lessc --strict-math=on ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`
|
||||
`node bin/lessc --strict-math=on ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`
|
||||
].join(" && ")
|
||||
},
|
||||
plugin: {
|
||||
command: [
|
||||
`node bin/lessc --clean-css="--s1 --advanced" ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||
`node bin/lessc --clean-css="--s1 --advanced" ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`,
|
||||
"cd lib",
|
||||
`node ../bin/lessc --clean-css="--s1 --advanced" ../${lessFolder}/_main/lazy-eval.less ../tmp/lazy-eval.css`,
|
||||
`node ../bin/lessc --source-map=lazy-eval.css.map --autoprefix ../${lessFolder}/_main/lazy-eval.less ../tmp/lazy-eval.css`,
|
||||
`node ../bin/lessc --clean-css="--s1 --advanced" ../${lessFolder}/tests-unit/lazy-eval/lazy-eval.less ../tmp/lazy-eval.css`,
|
||||
`node ../bin/lessc --source-map=lazy-eval.css.map --autoprefix ../${lessFolder}/tests-unit/lazy-eval/lazy-eval.less ../tmp/lazy-eval.css`,
|
||||
"cd ..",
|
||||
// Test multiple plugins
|
||||
`node bin/lessc --plugin=clean-css="--s1 --advanced" --plugin=autoprefix="ie 11,Edge >= 13,Chrome >= 47,Firefox >= 45,iOS >= 9.2,Safari >= 9" ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`
|
||||
`node bin/lessc --plugin=clean-css="--s1 --advanced" --plugin=autoprefix="ie 11,Edge >= 13,Chrome >= 47,Firefox >= 45,iOS >= 9.2,Safari >= 9" ${lessFolder}/tests-unit/lazy-eval/lazy-eval.less tmp/lazy-eval.css`
|
||||
].join(" && ")
|
||||
},
|
||||
"sourcemap-test": {
|
||||
// quoted value doesn't seem to get picked up by time-grunt, or isn't output, at least; maybe just "sourcemap" is fine?
|
||||
command: [
|
||||
`node bin/lessc --source-map=test/sourcemaps/maps/import-map.map ${lessFolder}/_main/import.less test/sourcemaps/import.css`,
|
||||
`node bin/lessc --source-map ${lessFolder}/sourcemaps/basic.less test/sourcemaps/basic.css`
|
||||
`node bin/lessc --source-map=test/sourcemaps/maps/import-map.map ${lessFolder}/tests-unit/import/import.less test/sourcemaps/import.css`,
|
||||
`node bin/lessc --source-map ${lessFolder}/tests-config/sourcemaps/basic.less test/sourcemaps/basic.css`
|
||||
].join(" && ")
|
||||
}
|
||||
},
|
||||
|
||||
@@ -98,53 +98,48 @@ function render() {
|
||||
}
|
||||
|
||||
if (options.sourceMap) {
|
||||
sourceMapOptions.sourceMapInputFilename = input;
|
||||
|
||||
if (!sourceMapOptions.sourceMapFullFilename) {
|
||||
if (!output && !sourceMapFileInline) {
|
||||
console.error('the sourcemap option only has an optional filename if the css filename is given');
|
||||
console.error('consider adding --source-map-map-inline which embeds the sourcemap into the css');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
} // its in the same directory, so always just the basename
|
||||
|
||||
|
||||
if (output) {
|
||||
sourceMapOptions.sourceMapOutputFilename = path.basename(output);
|
||||
sourceMapOptions.sourceMapFullFilename = ''.concat(output, '.map');
|
||||
} // its in the same directory, so always just the basename
|
||||
|
||||
|
||||
if ('sourceMapFullFilename' in sourceMapOptions) {
|
||||
sourceMapOptions.sourceMapFilename = path.basename(sourceMapOptions.sourceMapFullFilename);
|
||||
}
|
||||
} else if (options.sourceMap && !sourceMapFileInline) {
|
||||
var mapFilename = path.resolve(process.cwd(), sourceMapOptions.sourceMapFullFilename);
|
||||
var mapDir = path.dirname(mapFilename);
|
||||
var outputDir = path.dirname(output); // find the path from the map to the output file
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
sourceMapOptions.sourceMapOutputFilename = path.join(path.relative(mapDir, outputDir), path.basename(output)); // make the sourcemap filename point to the sourcemap relative to the css file output directory
|
||||
|
||||
sourceMapOptions.sourceMapFilename = path.join(path.relative(outputDir, mapDir), path.basename(sourceMapOptions.sourceMapFullFilename));
|
||||
}
|
||||
|
||||
// Validate conflicting options
|
||||
if (sourceMapOptions.sourceMapURL && sourceMapOptions.disableSourcemapAnnotation) {
|
||||
console.error('You cannot provide flag --source-map-url with --source-map-no-annotation.');
|
||||
console.error('Please remove one of those flags.');
|
||||
process.exitcode = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceMapOptions.sourceMapBasepath === undefined) {
|
||||
sourceMapOptions.sourceMapBasepath = input ? path.dirname(input) : process.cwd();
|
||||
}
|
||||
|
||||
if (sourceMapOptions.sourceMapRootpath === undefined) {
|
||||
var pathToMap = path.dirname((sourceMapFileInline ? output : sourceMapOptions.sourceMapFullFilename) || '.');
|
||||
var pathToInput = path.dirname(sourceMapOptions.sourceMapInputFilename || '.');
|
||||
sourceMapOptions.sourceMapRootpath = path.relative(pathToMap, pathToInput);
|
||||
// Handle explicit sourceMapFullFilename (from --source-map=filename)
|
||||
// Normalization of other options (sourceMapBasepath, sourceMapRootpath, etc.)
|
||||
// is handled automatically in parse-tree.js
|
||||
if (sourceMapOptions.sourceMapFullFilename && !sourceMapFileInline) {
|
||||
var mapFilename = path.resolve(process.cwd(), sourceMapOptions.sourceMapFullFilename);
|
||||
var mapDir = path.dirname(mapFilename);
|
||||
|
||||
if (output) {
|
||||
var outputDir = path.dirname(output);
|
||||
// Set sourceMapOutputFilename relative to map directory
|
||||
sourceMapOptions.sourceMapOutputFilename = path.join(
|
||||
path.relative(mapDir, outputDir),
|
||||
path.basename(output)
|
||||
);
|
||||
// Set sourceMapFilename relative to output directory (for sourceMappingURL comment)
|
||||
sourceMapOptions.sourceMapFilename = path.join(
|
||||
path.relative(outputDir, mapDir),
|
||||
path.basename(sourceMapOptions.sourceMapFullFilename)
|
||||
);
|
||||
} else {
|
||||
// No output filename, just use basename
|
||||
sourceMapOptions.sourceMapOutputFilename = path.basename(output || 'output.css');
|
||||
sourceMapOptions.sourceMapFilename = path.basename(sourceMapOptions.sourceMapFullFilename);
|
||||
}
|
||||
} else if (!sourceMapOptions.sourceMapFullFilename && output && !sourceMapFileInline) {
|
||||
// No explicit sourcemap filename, derive from output
|
||||
sourceMapOptions.sourceMapOutputFilename = path.basename(output);
|
||||
sourceMapOptions.sourceMapFullFilename = ''.concat(output, '.map');
|
||||
} else if (!output && !sourceMapFileInline) {
|
||||
console.error('the sourcemap option only has an optional filename if the css filename is given');
|
||||
console.error('consider adding --source-map-map-inline which embeds the sourcemap into the css');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"scripts": {
|
||||
"quicktest": "grunt quicktest",
|
||||
"test": "grunt test",
|
||||
"test:coverage": "c8 -r lcov -r json-summary -r text-summary -r html --include=\"lib/**/*.js\" --include=\"bin/**/*.js\" --exclude=\"dist/**\" --exclude=\"**/*.test.js\" --exclude=\"**/*.spec.js\" --exclude=\"test/**\" --exclude=\"tmp/**\" --exclude=\"**/abstract-file-manager.js\" --exclude=\"**/abstract-plugin-loader.js\" grunt shell:test && node scripts/coverage-report.js && node scripts/coverage-lines.js",
|
||||
"grunt": "grunt",
|
||||
"lint": "eslint '**/*.{ts,js}'",
|
||||
"lint:fix": "eslint '**/*.{ts,js}' --fix",
|
||||
@@ -44,7 +45,8 @@
|
||||
"clean": "shx rm -rf ./lib tsconfig.tsbuildinfo",
|
||||
"compile": "tsc -p tsconfig.build.json",
|
||||
"dev": "tsc -p tsconfig.build.json -w",
|
||||
"prepublishOnly": "grunt dist"
|
||||
"prepublishOnly": "grunt dist",
|
||||
"postinstall": "node scripts/postinstall.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"errno": "^0.1.1",
|
||||
@@ -66,11 +68,14 @@
|
||||
"benny": "^3.6.12",
|
||||
"bootstrap-less-port": "0.3.0",
|
||||
"chai": "^4.2.0",
|
||||
"c8": "^10.1.3",
|
||||
"chalk": "^4.1.2",
|
||||
"cosmiconfig": "~9.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"diff": "^3.2.0",
|
||||
"eslint": "^7.29.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"git-rev": "^0.2.1",
|
||||
"glob": "~11.0.3",
|
||||
"globby": "^10.0.1",
|
||||
"grunt": "^1.0.4",
|
||||
"grunt-cli": "^1.3.2",
|
||||
@@ -80,17 +85,17 @@
|
||||
"grunt-saucelabs": "^9.0.1",
|
||||
"grunt-shell": "^1.3.0",
|
||||
"html-template-tag": "^3.2.0",
|
||||
"jest-diff": "~30.1.2",
|
||||
"jit-grunt": "^0.10.0",
|
||||
"less-plugin-autoprefix": "^1.5.1",
|
||||
"less-plugin-clean-css": "^1.6.0",
|
||||
"minimist": "^1.2.0",
|
||||
"mocha": "^6.2.1",
|
||||
"playwright": "1.50.1",
|
||||
"mocha-teamcity-reporter": "^3.0.0",
|
||||
"nock": "^11.8.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"performance-now": "^0.2.0",
|
||||
"phin": "^2.2.3",
|
||||
"playwright": "1.50.1",
|
||||
"promise": "^7.1.1",
|
||||
"read-glob": "^3.0.0",
|
||||
"resolve": "^1.17.0",
|
||||
|
||||
207
packages/less/scripts/coverage-lines.js
Normal file
207
packages/less/scripts/coverage-lines.js
Normal file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generates a line-by-line coverage report showing uncovered lines
|
||||
* Reads from LCOV format and displays in terminal
|
||||
* Also outputs JSON file with uncovered lines for programmatic access
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const lcovPath = path.join(__dirname, '..', 'coverage', 'lcov.info');
|
||||
const jsonOutputPath = path.join(__dirname, '..', 'coverage', 'uncovered-lines.json');
|
||||
|
||||
if (!fs.existsSync(lcovPath)) {
|
||||
console.error('LCOV coverage file not found. Run pnpm test:coverage first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const lcovContent = fs.readFileSync(lcovPath, 'utf8');
|
||||
|
||||
// Parse LCOV format
|
||||
const files = [];
|
||||
let currentFile = null;
|
||||
|
||||
const lines = lcovContent.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// SF: source file
|
||||
if (line.startsWith('SF:')) {
|
||||
if (currentFile) {
|
||||
files.push(currentFile);
|
||||
}
|
||||
const filePath = line.substring(3);
|
||||
// Only include src/ files (not less-browser) and bin/
|
||||
// Exclude abstract base classes (they're meant to be overridden)
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
const abstractClasses = ['abstract-file-manager', 'abstract-plugin-loader'];
|
||||
const isAbstract = abstractClasses.some(abstract => normalized.includes(abstract));
|
||||
|
||||
if (!isAbstract &&
|
||||
((normalized.includes('src/less/') && !normalized.includes('src/less-browser/')) ||
|
||||
normalized.includes('src/less-node/') ||
|
||||
normalized.includes('bin/'))) {
|
||||
// Extract relative path - match src/less/... or src/less-node/... or bin/...
|
||||
// Path format: src/less/tree/debug-info.js or src/less-node/file-manager.js
|
||||
// Match from src/ or bin/ to end of path
|
||||
const match = normalized.match(/(src\/[^/]+\/.+|bin\/.+)$/);
|
||||
const relativePath = match ? match[1] : (normalized.includes('/src/') || normalized.includes('/bin/') ? normalized.split('/').slice(-3).join('/') : path.basename(filePath));
|
||||
currentFile = {
|
||||
path: relativePath,
|
||||
fullPath: filePath,
|
||||
uncoveredLines: [],
|
||||
uncoveredLineCode: {}, // line number -> source code
|
||||
totalLines: 0,
|
||||
coveredLines: 0
|
||||
};
|
||||
} else {
|
||||
currentFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
// DA: line data (line number, execution count)
|
||||
if (currentFile && line.startsWith('DA:')) {
|
||||
const match = line.match(/^DA:(\d+),(\d+)$/);
|
||||
if (match) {
|
||||
const lineNum = parseInt(match[1], 10);
|
||||
const count = parseInt(match[2], 10);
|
||||
currentFile.totalLines++;
|
||||
if (count > 0) {
|
||||
currentFile.coveredLines++;
|
||||
} else {
|
||||
currentFile.uncoveredLines.push(lineNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentFile) {
|
||||
files.push(currentFile);
|
||||
}
|
||||
|
||||
// Read source code for uncovered lines
|
||||
files.forEach(file => {
|
||||
if (file.uncoveredLines.length > 0 && fs.existsSync(file.fullPath)) {
|
||||
try {
|
||||
const sourceCode = fs.readFileSync(file.fullPath, 'utf8');
|
||||
const sourceLines = sourceCode.split('\n');
|
||||
file.uncoveredLines.forEach(lineNum => {
|
||||
// LCOV uses 1-based line numbers
|
||||
if (lineNum > 0 && lineNum <= sourceLines.length) {
|
||||
file.uncoveredLineCode[lineNum] = sourceLines[lineNum - 1].trim();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
// If we can't read the source (e.g., it's in lib/ but we want src/), that's ok
|
||||
// We'll just skip the source code
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Filter to only files with uncovered lines and sort by coverage
|
||||
const filesWithGaps = files
|
||||
.filter(f => f.uncoveredLines.length > 0)
|
||||
.sort((a, b) => {
|
||||
const aPct = a.totalLines > 0 ? a.coveredLines / a.totalLines : 1;
|
||||
const bPct = b.totalLines > 0 ? b.coveredLines / b.totalLines : 1;
|
||||
return aPct - bPct;
|
||||
});
|
||||
|
||||
if (filesWithGaps.length === 0) {
|
||||
if (files.length === 0) {
|
||||
console.log('\n⚠️ No source files found in coverage data. This may indicate an issue with the coverage report.\n');
|
||||
} else {
|
||||
console.log('\n✅ All analyzed files have 100% line coverage!\n');
|
||||
console.log(`(Analyzed ${files.length} files from src/less/, src/less-node/, and bin/)\n`);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(100));
|
||||
console.log('Uncovered Lines Report');
|
||||
console.log('='.repeat(100) + '\n');
|
||||
|
||||
filesWithGaps.forEach(file => {
|
||||
const coveragePct = file.totalLines > 0
|
||||
? ((file.coveredLines / file.totalLines) * 100).toFixed(1)
|
||||
: '0.0';
|
||||
|
||||
console.log(`\n${file.path} (${coveragePct}% coverage)`);
|
||||
console.log('-'.repeat(100));
|
||||
|
||||
// Group consecutive lines into ranges
|
||||
const ranges = [];
|
||||
let start = file.uncoveredLines[0];
|
||||
let end = file.uncoveredLines[0];
|
||||
|
||||
for (let i = 1; i < file.uncoveredLines.length; i++) {
|
||||
if (file.uncoveredLines[i] === end + 1) {
|
||||
end = file.uncoveredLines[i];
|
||||
} else {
|
||||
ranges.push(start === end ? `${start}` : `${start}..${end}`);
|
||||
start = file.uncoveredLines[i];
|
||||
end = file.uncoveredLines[i];
|
||||
}
|
||||
}
|
||||
ranges.push(start === end ? `${start}` : `${start}..${end}`);
|
||||
|
||||
// Display ranges (max 5 per line for readability)
|
||||
const linesPerRow = 5;
|
||||
for (let i = 0; i < ranges.length; i += linesPerRow) {
|
||||
const row = ranges.slice(i, i + linesPerRow);
|
||||
console.log(` Lines: ${row.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(` Total uncovered: ${file.uncoveredLines.length} of ${file.totalLines} lines`);
|
||||
});
|
||||
|
||||
console.log('\n' + '='.repeat(100) + '\n');
|
||||
|
||||
// Write JSON output for programmatic access
|
||||
const jsonOutput = {
|
||||
generated: new Date().toISOString(),
|
||||
files: filesWithGaps.map(file => ({
|
||||
path: file.path,
|
||||
fullPath: file.fullPath,
|
||||
sourcePath: (() => {
|
||||
// Try to map lib/ path to src/ path
|
||||
const normalized = file.fullPath.replace(/\\/g, '/');
|
||||
if (normalized.includes('/lib/')) {
|
||||
return normalized.replace('/lib/', '/src/').replace(/\.js$/, '.ts');
|
||||
}
|
||||
return file.fullPath;
|
||||
})(),
|
||||
coveragePercent: file.totalLines > 0
|
||||
? parseFloat(((file.coveredLines / file.totalLines) * 100).toFixed(1))
|
||||
: 0,
|
||||
totalLines: file.totalLines,
|
||||
coveredLines: file.coveredLines,
|
||||
uncoveredLines: file.uncoveredLines,
|
||||
uncoveredLineCode: file.uncoveredLineCode || {},
|
||||
uncoveredRanges: (() => {
|
||||
const ranges = [];
|
||||
if (file.uncoveredLines.length === 0) return ranges;
|
||||
|
||||
let start = file.uncoveredLines[0];
|
||||
let end = file.uncoveredLines[0];
|
||||
|
||||
for (let i = 1; i < file.uncoveredLines.length; i++) {
|
||||
if (file.uncoveredLines[i] === end + 1) {
|
||||
end = file.uncoveredLines[i];
|
||||
} else {
|
||||
ranges.push({ start, end });
|
||||
start = file.uncoveredLines[i];
|
||||
end = file.uncoveredLines[i];
|
||||
}
|
||||
}
|
||||
ranges.push({ start, end });
|
||||
return ranges;
|
||||
})()
|
||||
}))
|
||||
};
|
||||
|
||||
fs.writeFileSync(jsonOutputPath, JSON.stringify(jsonOutput, null, 2), 'utf8');
|
||||
console.log('\n📄 Uncovered lines data written to: coverage/uncovered-lines.json\n');
|
||||
|
||||
158
packages/less/scripts/coverage-report.js
Normal file
158
packages/less/scripts/coverage-report.js
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generates a per-file coverage report table for src/ directories
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const coverageSummaryPath = path.join(__dirname, '..', 'coverage', 'coverage-summary.json');
|
||||
|
||||
if (!fs.existsSync(coverageSummaryPath)) {
|
||||
console.error('Coverage summary not found. Run pnpm test:coverage first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const coverage = JSON.parse(fs.readFileSync(coverageSummaryPath, 'utf8'));
|
||||
|
||||
// Filter to only src/ files (less, less-node) and bin/ files
|
||||
// Note: src/less-browser/ is excluded because browser tests aren't included in coverage
|
||||
// Abstract base classes are excluded as they're meant to be overridden by implementations
|
||||
const abstractClasses = [
|
||||
'abstract-file-manager',
|
||||
'abstract-plugin-loader'
|
||||
];
|
||||
|
||||
const srcFiles = Object.entries(coverage)
|
||||
.filter(([filePath]) => {
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
// Exclude abstract classes
|
||||
if (abstractClasses.some(abstract => normalized.includes(abstract))) {
|
||||
return false;
|
||||
}
|
||||
return (normalized.includes('/src/less/') && !normalized.includes('/src/less-browser/')) ||
|
||||
normalized.includes('/src/less-node/') ||
|
||||
normalized.includes('/bin/');
|
||||
})
|
||||
.map(([filePath, data]) => {
|
||||
// Extract relative path from absolute path
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
// Match src/ paths or bin/ paths
|
||||
const match = normalized.match(/((?:src\/[^/]+\/[^/]+\/|bin\/).+)$/);
|
||||
const relativePath = match ? match[1] : path.basename(filePath);
|
||||
|
||||
return {
|
||||
path: relativePath,
|
||||
statements: data.statements,
|
||||
branches: data.branches,
|
||||
functions: data.functions,
|
||||
lines: data.lines
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Sort by directory first, then by coverage percentage
|
||||
const pathCompare = a.path.localeCompare(b.path);
|
||||
if (pathCompare !== 0) return pathCompare;
|
||||
return a.statements.pct - b.statements.pct;
|
||||
});
|
||||
|
||||
if (srcFiles.length === 0) {
|
||||
console.log('No src/ files found in coverage report.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Group by directory
|
||||
const grouped = {
|
||||
'src/less/': [],
|
||||
'src/less-node/': [],
|
||||
'bin/': []
|
||||
};
|
||||
|
||||
srcFiles.forEach(file => {
|
||||
if (file.path.startsWith('src/less/')) {
|
||||
grouped['src/less/'].push(file);
|
||||
} else if (file.path.startsWith('src/less-node/')) {
|
||||
grouped['src/less-node/'].push(file);
|
||||
} else if (file.path.startsWith('bin/')) {
|
||||
grouped['bin/'].push(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Print table
|
||||
console.log('\n' + '='.repeat(100));
|
||||
console.log('Per-File Coverage Report (src/less/, src/less-node/, and bin/)');
|
||||
console.log('='.repeat(100));
|
||||
console.log('For line-by-line coverage details, open coverage/index.html in your browser.');
|
||||
console.log('='.repeat(100) + '\n');
|
||||
|
||||
Object.entries(grouped).forEach(([dir, files]) => {
|
||||
if (files.length === 0) return;
|
||||
|
||||
console.log(`\n${dir.toUpperCase()}`);
|
||||
console.log('-'.repeat(100));
|
||||
console.log(
|
||||
'File'.padEnd(50) +
|
||||
'Statements'.padStart(12) +
|
||||
'Branches'.padStart(12) +
|
||||
'Functions'.padStart(12) +
|
||||
'Lines'.padStart(12)
|
||||
);
|
||||
console.log('-'.repeat(100));
|
||||
|
||||
files.forEach(file => {
|
||||
const filename = file.path.replace(dir, '');
|
||||
const truncated = filename.length > 48 ? '...' + filename.slice(-45) : filename;
|
||||
|
||||
console.log(
|
||||
truncated.padEnd(50) +
|
||||
`${file.statements.pct.toFixed(1)}%`.padStart(12) +
|
||||
`${file.branches.pct.toFixed(1)}%`.padStart(12) +
|
||||
`${file.functions.pct.toFixed(1)}%`.padStart(12) +
|
||||
`${file.lines.pct.toFixed(1)}%`.padStart(12)
|
||||
);
|
||||
});
|
||||
|
||||
// Summary for this directory
|
||||
const totals = files.reduce((acc, file) => {
|
||||
acc.statements.total += file.statements.total;
|
||||
acc.statements.covered += file.statements.covered;
|
||||
acc.branches.total += file.branches.total;
|
||||
acc.branches.covered += file.branches.covered;
|
||||
acc.functions.total += file.functions.total;
|
||||
acc.functions.covered += file.functions.covered;
|
||||
acc.lines.total += file.lines.total;
|
||||
acc.lines.covered += file.lines.covered;
|
||||
return acc;
|
||||
}, {
|
||||
statements: { total: 0, covered: 0 },
|
||||
branches: { total: 0, covered: 0 },
|
||||
functions: { total: 0, covered: 0 },
|
||||
lines: { total: 0, covered: 0 }
|
||||
});
|
||||
|
||||
const stmtPct = totals.statements.total > 0
|
||||
? (totals.statements.covered / totals.statements.total * 100).toFixed(1)
|
||||
: '0.0';
|
||||
const branchPct = totals.branches.total > 0
|
||||
? (totals.branches.covered / totals.branches.total * 100).toFixed(1)
|
||||
: '0.0';
|
||||
const funcPct = totals.functions.total > 0
|
||||
? (totals.functions.covered / totals.functions.total * 100).toFixed(1)
|
||||
: '0.0';
|
||||
const linePct = totals.lines.total > 0
|
||||
? (totals.lines.covered / totals.lines.total * 100).toFixed(1)
|
||||
: '0.0';
|
||||
|
||||
console.log('-'.repeat(100));
|
||||
console.log(
|
||||
'TOTAL'.padEnd(50) +
|
||||
`${stmtPct}%`.padStart(12) +
|
||||
`${branchPct}%`.padStart(12) +
|
||||
`${funcPct}%`.padStart(12) +
|
||||
`${linePct}%`.padStart(12)
|
||||
);
|
||||
});
|
||||
|
||||
console.log('\n' + '='.repeat(100) + '\n');
|
||||
|
||||
61
packages/less/scripts/postinstall.js
Normal file
61
packages/less/scripts/postinstall.js
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Post-install script for Less.js package
|
||||
*
|
||||
* This script installs Playwright browsers only when:
|
||||
* 1. This is a development environment (not when installed as a dependency)
|
||||
* 2. We're in a monorepo context (parent package.json exists)
|
||||
* 3. Not running in CI or other automated environments
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Check if we're in a development environment
|
||||
function isDevelopmentEnvironment() {
|
||||
// Skip if this is a global install or user config
|
||||
if (process.env.npm_config_user_config || process.env.npm_config_global) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip in CI environments
|
||||
if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.TRAVIS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're in a monorepo (parent package.json exists)
|
||||
const parentPackageJson = path.join(__dirname, '../../../package.json');
|
||||
if (!fs.existsSync(parentPackageJson)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this is the root of the monorepo
|
||||
const currentPackageJson = path.join(__dirname, '../package.json');
|
||||
if (!fs.existsSync(currentPackageJson)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Install Playwright browsers
|
||||
function installPlaywrightBrowsers() {
|
||||
try {
|
||||
console.log('🎭 Installing Playwright browsers for development...');
|
||||
execSync('pnpm exec playwright install', {
|
||||
stdio: 'inherit',
|
||||
cwd: path.join(__dirname, '..')
|
||||
});
|
||||
console.log('✅ Playwright browsers installed successfully');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to install Playwright browsers:', error.message);
|
||||
console.warn(' You can install them manually with: pnpm exec playwright install');
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
if (isDevelopmentEnvironment()) {
|
||||
installPlaywrightBrowsers();
|
||||
}
|
||||
@@ -34,7 +34,7 @@ const lessc_helper = {
|
||||
console.log(' -l, --lint Syntax check only (lint).');
|
||||
console.log(' -s, --silent Suppresses output of error messages.');
|
||||
console.log(' --quiet Suppresses output of warnings.');
|
||||
console.log(' --strict-imports Forces evaluation of imports.');
|
||||
console.log(' --strict-imports (DEPRECATED) Ignores .less imports inside selector blocks. Has confusing behavior.');
|
||||
console.log(' --insecure Allows imports from insecure https hosts.');
|
||||
console.log(' -v, --version Prints version number and exit.');
|
||||
console.log(' --verbose Be verbose.');
|
||||
@@ -74,12 +74,13 @@ const lessc_helper = {
|
||||
console.log(' -sm=on|off Legacy parens-only math. Use --math');
|
||||
console.log(' --strict-math=on|off ');
|
||||
console.log('');
|
||||
console.log(' --line-numbers=TYPE Outputs filename and line numbers.');
|
||||
console.log(' TYPE can be either \'comments\', which will output');
|
||||
console.log(' the debug info within comments, \'mediaquery\'');
|
||||
console.log(' that will output the information within a fake');
|
||||
console.log(' media query which is compatible with the SASS');
|
||||
console.log(' format, and \'all\' which will do both.');
|
||||
console.log(' --line-numbers=TYPE (DEPRECATED) Outputs filename and line numbers.');
|
||||
console.log(' TYPE can be either \'comments\', \'mediaquery\', or \'all\'.');
|
||||
console.log(' The entire dumpLineNumbers option is deprecated.');
|
||||
console.log(' Use sourcemaps (--source-map) instead.');
|
||||
console.log(' All modes will be removed in a future version.');
|
||||
console.log(' Note: \'mediaquery\' and \'all\' modes generate @media -sass-debug-info');
|
||||
console.log(' which had short-lived usage and is no longer recommended.');
|
||||
console.log(' -x, --compress Compresses output by removing some whitespaces.');
|
||||
console.log(' We recommend you use a dedicated minifer like less-plugin-clean-css');
|
||||
console.log('');
|
||||
|
||||
@@ -22,10 +22,9 @@ const parseCopyProperties = [
|
||||
'rootpath', // option - rootpath to append to URL's
|
||||
'strictImports', // option -
|
||||
'insecure', // option - whether to allow imports from insecure ssl hosts
|
||||
'dumpLineNumbers', // option - whether to dump line numbers
|
||||
'dumpLineNumbers', // option - @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. All modes ('comments', 'mediaquery', 'all') will be removed in a future version.
|
||||
'compress', // option - whether to compress
|
||||
'syncImport', // option - whether to import synchronously
|
||||
'chunkInput', // option - whether to chunk input. more performant but causes parse issues.
|
||||
'mime', // browser only - mime type for sheet import
|
||||
'useFileCache', // browser only - whether to use the per file session cache
|
||||
// context
|
||||
|
||||
@@ -25,9 +25,28 @@ export default function() {
|
||||
/* color output in the terminal */
|
||||
color: true,
|
||||
|
||||
/* The strictImports controls whether the compiler will allow an @import inside of either
|
||||
* @media blocks or (a later addition) other selector blocks.
|
||||
* See: https://github.com/less/less.js/issues/656 */
|
||||
/**
|
||||
* @deprecated This option has confusing behavior and may be removed in a future version.
|
||||
*
|
||||
* When true, prevents @import statements for .less files from being evaluated inside
|
||||
* selector blocks (rulesets). The imports are silently ignored and not output.
|
||||
*
|
||||
* Behavior:
|
||||
* - @import at root level: Always processed
|
||||
* - @import inside @-rules (@media, @supports, etc.): Processed (these are not selector blocks)
|
||||
* - @import inside selector blocks (.class, #id, etc.): NOT processed (silently ignored)
|
||||
*
|
||||
* When false (default): All @import statements are processed regardless of context.
|
||||
*
|
||||
* Note: Despite the name "strict", this option does NOT throw an error when imports
|
||||
* are used in selector blocks - it silently ignores them. This is confusing
|
||||
* behavior that may catch users off guard.
|
||||
*
|
||||
* Note: Only affects .less file imports. CSS imports (url(...) or .css files) are
|
||||
* always output as CSS @import statements regardless of this setting.
|
||||
*
|
||||
* @see https://github.com/less/less.js/issues/656
|
||||
*/
|
||||
strictImports: false,
|
||||
|
||||
/* Allow Imports from Insecure HTTPS Hosts */
|
||||
|
||||
@@ -31,6 +31,9 @@ const LessError = function(e, fileContentMap, currentFilename) {
|
||||
|
||||
this.message = e.message;
|
||||
this.stack = e.stack;
|
||||
|
||||
// Set type early so it's always available, even if fileContentMap is missing
|
||||
this.type = e.type || 'Syntax';
|
||||
|
||||
if (fileContentMap && filename) {
|
||||
const input = fileContentMap.contents[filename];
|
||||
@@ -40,7 +43,6 @@ const LessError = function(e, fileContentMap, currentFilename) {
|
||||
const callLine = e.call && utils.getLocation(e.call, input).line;
|
||||
const lines = input ? input.split('\n') : '';
|
||||
|
||||
this.type = e.type || 'Syntax';
|
||||
this.filename = filename;
|
||||
this.index = e.index;
|
||||
this.line = typeof line === 'number' ? line + 1 : null;
|
||||
|
||||
@@ -28,12 +28,70 @@ export default function(SourceMapBuilder) {
|
||||
|
||||
const toCSSOptions = {
|
||||
compress,
|
||||
// @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. All modes will be removed in a future version.
|
||||
dumpLineNumbers: options.dumpLineNumbers,
|
||||
strictUnits: Boolean(options.strictUnits),
|
||||
numPrecision: 8};
|
||||
|
||||
if (options.sourceMap) {
|
||||
sourceMapBuilder = new SourceMapBuilder(options.sourceMap);
|
||||
// Normalize sourceMap option: if it's just true, convert to object
|
||||
if (options.sourceMap === true) {
|
||||
options.sourceMap = {};
|
||||
}
|
||||
const sourceMapOpts = options.sourceMap;
|
||||
|
||||
// Set sourceMapInputFilename if not set and filename is available
|
||||
if (!sourceMapOpts.sourceMapInputFilename && options.filename) {
|
||||
sourceMapOpts.sourceMapInputFilename = options.filename;
|
||||
}
|
||||
|
||||
// Default sourceMapBasepath to the input file's directory if not set
|
||||
// This matches the behavior documented and implemented in bin/lessc
|
||||
if (sourceMapOpts.sourceMapBasepath === undefined && options.filename) {
|
||||
// Get directory from filename using string manipulation (works cross-platform)
|
||||
const lastSlash = Math.max(options.filename.lastIndexOf('/'), options.filename.lastIndexOf('\\'));
|
||||
if (lastSlash >= 0) {
|
||||
sourceMapOpts.sourceMapBasepath = options.filename.substring(0, lastSlash);
|
||||
} else {
|
||||
// No directory separator found, use current directory
|
||||
sourceMapOpts.sourceMapBasepath = '.';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sourceMapFullFilename (CLI-specific: --source-map=filename)
|
||||
// This is converted to sourceMapFilename and sourceMapOutputFilename
|
||||
if (sourceMapOpts.sourceMapFullFilename && !sourceMapOpts.sourceMapFileInline) {
|
||||
// This case is handled by lessc before calling render
|
||||
// We just need to ensure sourceMapFilename is set if sourceMapFullFilename is provided
|
||||
if (!sourceMapOpts.sourceMapFilename && !sourceMapOpts.sourceMapURL) {
|
||||
// Extract just the basename for the sourceMappingURL comment
|
||||
const mapBase = sourceMapOpts.sourceMapFullFilename.split(/[/\\]/).pop();
|
||||
sourceMapOpts.sourceMapFilename = mapBase;
|
||||
}
|
||||
} else if (!sourceMapOpts.sourceMapFilename && !sourceMapOpts.sourceMapURL) {
|
||||
// If sourceMapFilename is not set and sourceMapURL is not set,
|
||||
// derive it from the output filename (if available) or input filename
|
||||
if (sourceMapOpts.sourceMapOutputFilename) {
|
||||
// Use output filename + .map
|
||||
sourceMapOpts.sourceMapFilename = sourceMapOpts.sourceMapOutputFilename + '.map';
|
||||
} else if (options.filename) {
|
||||
// Fallback to input filename + .css.map
|
||||
const inputBase = options.filename.replace(/\.[^/.]+$/, '');
|
||||
sourceMapOpts.sourceMapFilename = inputBase + '.css.map';
|
||||
}
|
||||
}
|
||||
|
||||
// Default sourceMapOutputFilename if not set
|
||||
if (!sourceMapOpts.sourceMapOutputFilename) {
|
||||
if (options.filename) {
|
||||
const inputBase = options.filename.replace(/\.[^/.]+$/, '');
|
||||
sourceMapOpts.sourceMapOutputFilename = inputBase + '.css';
|
||||
} else {
|
||||
sourceMapOpts.sourceMapOutputFilename = 'output.css';
|
||||
}
|
||||
}
|
||||
|
||||
sourceMapBuilder = new SourceMapBuilder(sourceMapOpts);
|
||||
result.css = sourceMapBuilder.toCSS(evaldRoot, toCSSOptions, this.imports);
|
||||
} else {
|
||||
result.css = evaldRoot.toCSS(toCSSOptions);
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
// Split the input into chunks.
|
||||
export default function (input, fail) {
|
||||
const len = input.length;
|
||||
let level = 0;
|
||||
let parenLevel = 0;
|
||||
let lastOpening;
|
||||
let lastOpeningParen;
|
||||
let lastMultiComment;
|
||||
let lastMultiCommentEndBrace;
|
||||
const chunks = [];
|
||||
let emitFrom = 0;
|
||||
let chunkerCurrentIndex;
|
||||
let currentChunkStartIndex;
|
||||
let cc;
|
||||
let cc2;
|
||||
let matched;
|
||||
|
||||
function emitChunk(force) {
|
||||
const len = chunkerCurrentIndex - emitFrom;
|
||||
if (((len < 512) && !force) || !len) {
|
||||
return;
|
||||
}
|
||||
chunks.push(input.slice(emitFrom, chunkerCurrentIndex + 1));
|
||||
emitFrom = chunkerCurrentIndex + 1;
|
||||
}
|
||||
|
||||
for (chunkerCurrentIndex = 0; chunkerCurrentIndex < len; chunkerCurrentIndex++) {
|
||||
cc = input.charCodeAt(chunkerCurrentIndex);
|
||||
if (((cc >= 97) && (cc <= 122)) || (cc < 34)) {
|
||||
// a-z or whitespace
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (cc) {
|
||||
case 40: // (
|
||||
parenLevel++;
|
||||
lastOpeningParen = chunkerCurrentIndex;
|
||||
continue;
|
||||
case 41: // )
|
||||
if (--parenLevel < 0) {
|
||||
return fail('missing opening `(`', chunkerCurrentIndex);
|
||||
}
|
||||
continue;
|
||||
case 59: // ;
|
||||
if (!parenLevel) { emitChunk(); }
|
||||
continue;
|
||||
case 123: // {
|
||||
level++;
|
||||
lastOpening = chunkerCurrentIndex;
|
||||
continue;
|
||||
case 125: // }
|
||||
if (--level < 0) {
|
||||
return fail('missing opening `{`', chunkerCurrentIndex);
|
||||
}
|
||||
if (!level && !parenLevel) { emitChunk(); }
|
||||
continue;
|
||||
case 92: // \
|
||||
if (chunkerCurrentIndex < len - 1) { chunkerCurrentIndex++; continue; }
|
||||
return fail('unescaped `\\`', chunkerCurrentIndex);
|
||||
case 34:
|
||||
case 39:
|
||||
case 96: // ", ' and `
|
||||
matched = 0;
|
||||
currentChunkStartIndex = chunkerCurrentIndex;
|
||||
for (chunkerCurrentIndex = chunkerCurrentIndex + 1; chunkerCurrentIndex < len; chunkerCurrentIndex++) {
|
||||
cc2 = input.charCodeAt(chunkerCurrentIndex);
|
||||
if (cc2 > 96) { continue; }
|
||||
if (cc2 == cc) { matched = 1; break; }
|
||||
if (cc2 == 92) { // \
|
||||
if (chunkerCurrentIndex == len - 1) {
|
||||
return fail('unescaped `\\`', chunkerCurrentIndex);
|
||||
}
|
||||
chunkerCurrentIndex++;
|
||||
}
|
||||
}
|
||||
if (matched) { continue; }
|
||||
return fail(`unmatched \`${String.fromCharCode(cc)}\``, currentChunkStartIndex);
|
||||
case 47: // /, check for comment
|
||||
if (parenLevel || (chunkerCurrentIndex == len - 1)) { continue; }
|
||||
cc2 = input.charCodeAt(chunkerCurrentIndex + 1);
|
||||
if (cc2 == 47) {
|
||||
// //, find lnfeed
|
||||
for (chunkerCurrentIndex = chunkerCurrentIndex + 2; chunkerCurrentIndex < len; chunkerCurrentIndex++) {
|
||||
cc2 = input.charCodeAt(chunkerCurrentIndex);
|
||||
if ((cc2 <= 13) && ((cc2 == 10) || (cc2 == 13))) { break; }
|
||||
}
|
||||
} else if (cc2 == 42) {
|
||||
// /*, find */
|
||||
lastMultiComment = currentChunkStartIndex = chunkerCurrentIndex;
|
||||
for (chunkerCurrentIndex = chunkerCurrentIndex + 2; chunkerCurrentIndex < len - 1; chunkerCurrentIndex++) {
|
||||
cc2 = input.charCodeAt(chunkerCurrentIndex);
|
||||
if (cc2 == 125) { lastMultiCommentEndBrace = chunkerCurrentIndex; }
|
||||
if (cc2 != 42) { continue; }
|
||||
if (input.charCodeAt(chunkerCurrentIndex + 1) == 47) { break; }
|
||||
}
|
||||
if (chunkerCurrentIndex == len - 1) {
|
||||
return fail('missing closing `*/`', currentChunkStartIndex);
|
||||
}
|
||||
chunkerCurrentIndex++;
|
||||
}
|
||||
continue;
|
||||
case 42: // *, check for unmatched */
|
||||
if ((chunkerCurrentIndex < len - 1) && (input.charCodeAt(chunkerCurrentIndex + 1) == 47)) {
|
||||
return fail('unmatched `/*`', chunkerCurrentIndex);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (level !== 0) {
|
||||
if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) {
|
||||
return fail('missing closing `}` or `*/`', lastOpening);
|
||||
} else {
|
||||
return fail('missing closing `}`', lastOpening);
|
||||
}
|
||||
} else if (parenLevel !== 0) {
|
||||
return fail('missing closing `)`', lastOpeningParen);
|
||||
}
|
||||
|
||||
emitChunk(true);
|
||||
return chunks;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import chunker from './chunker';
|
||||
|
||||
export default () => {
|
||||
let // Less input string
|
||||
input;
|
||||
@@ -351,26 +349,11 @@ export default () => {
|
||||
return (c > CHARCODE_9 || c < CHARCODE_PLUS) || c === CHARCODE_FORWARD_SLASH || c === CHARCODE_COMMA;
|
||||
};
|
||||
|
||||
parserInput.start = (str, chunkInput, failFunction) => {
|
||||
parserInput.start = (str) => {
|
||||
input = str;
|
||||
parserInput.i = j = currentPos = furthest = 0;
|
||||
|
||||
// chunking apparently makes things quicker (but my tests indicate
|
||||
// it might actually make things slower in node at least)
|
||||
// and it is a non-perfect parse - it can't recognise
|
||||
// unquoted urls, meaning it can't distinguish comments
|
||||
// meaning comments with quotes or {}() in them get 'counted'
|
||||
// and then lead to parse errors.
|
||||
// In addition if the chunking chunks in the wrong place we might
|
||||
// not be able to parse a parser statement in one go
|
||||
// this is officially deprecated but can be switched on via an option
|
||||
// in the case it causes too much performance issues.
|
||||
if (chunkInput) {
|
||||
chunks = chunker(str, failFunction);
|
||||
} else {
|
||||
chunks = [str];
|
||||
}
|
||||
|
||||
chunks = [str];
|
||||
current = chunks[0];
|
||||
|
||||
skipWhitespace(0);
|
||||
|
||||
@@ -124,12 +124,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
||||
const parser = parserInput;
|
||||
|
||||
try {
|
||||
parser.start(str, false, function fail(msg, index) {
|
||||
callback({
|
||||
message: msg,
|
||||
index: index + currentIndex
|
||||
});
|
||||
});
|
||||
parser.start(str);
|
||||
for (let x = 0, p; (p = parseList[x]); x++) {
|
||||
result = parsers[p]();
|
||||
returnNodes.push(result || null);
|
||||
@@ -209,14 +204,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
||||
// with the `root` property set to true, so no `{}` are
|
||||
// output. The callback is called when the input is parsed.
|
||||
try {
|
||||
parserInput.start(str, context.chunkInput, function fail(msg, index) {
|
||||
throw new LessError({
|
||||
index,
|
||||
type: 'Parse',
|
||||
message: msg,
|
||||
filename: fileInfo.filename
|
||||
}, imports);
|
||||
});
|
||||
parserInput.start(str);
|
||||
|
||||
tree.Node.prototype.parse = this;
|
||||
root = new tree.Ruleset(null, this.parsers.primary());
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function (environment) {
|
||||
if (options.sourceMapFilename) {
|
||||
this._sourceMapFilename = options.sourceMapFilename.replace(/\\/g, '/');
|
||||
}
|
||||
this._outputFilename = options.outputFilename;
|
||||
this._outputFilename = options.outputFilename ? options.outputFilename.replace(/\\/g, '/') : options.outputFilename;
|
||||
this.sourceMapURL = options.sourceMapURL;
|
||||
if (options.sourceMapBasepath) {
|
||||
this._sourceMapBasepath = options.sourceMapBasepath.replace(/\\/g, '/');
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
/**
|
||||
* @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead.
|
||||
* This will be removed in a future version.
|
||||
*
|
||||
* @param {Object} ctx - Context object with debugInfo
|
||||
* @returns {string} Debug info as CSS comment
|
||||
*/
|
||||
function asComment(ctx) {
|
||||
return `/* line ${ctx.debugInfo.lineNumber}, ${ctx.debugInfo.fileName} */\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead.
|
||||
* This function generates Sass-compatible debug info using @media -sass-debug-info syntax.
|
||||
* This format had short-lived usage and is no longer recommended.
|
||||
* This will be removed in a future version.
|
||||
*
|
||||
* @param {Object} ctx - Context object with debugInfo
|
||||
* @returns {string} Sass-compatible debug info as @media query
|
||||
*/
|
||||
function asMediaQuery(ctx) {
|
||||
let filenameWithProtocol = ctx.debugInfo.fileName;
|
||||
if (!/^[a-z]+:\/\//i.test(filenameWithProtocol)) {
|
||||
@@ -15,6 +31,19 @@ function asMediaQuery(ctx) {
|
||||
})}}line{font-family:\\00003${ctx.debugInfo.lineNumber}}}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates debug information (line numbers) for CSS output.
|
||||
*
|
||||
* @param {Object} context - Context object with dumpLineNumbers option
|
||||
* @param {Object} ctx - Context object with debugInfo
|
||||
* @param {string} [lineSeparator] - Separator between comment and media query (for 'all' mode)
|
||||
* @returns {string} Debug info string
|
||||
*
|
||||
* @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead.
|
||||
* All modes ('comments', 'mediaquery', 'all') are deprecated and will be removed in a future version.
|
||||
* The 'mediaquery' and 'all' modes generate Sass-compatible @media -sass-debug-info output
|
||||
* which had short-lived usage and is no longer recommended.
|
||||
*/
|
||||
function debugInfo(context, ctx, lineSeparator) {
|
||||
let result = '';
|
||||
if (context.dumpLineNumbers && !context.compress) {
|
||||
|
||||
1
packages/less/test.less
Normal file
1
packages/less/test.less
Normal file
@@ -0,0 +1 @@
|
||||
.test { color: red; }
|
||||
@@ -99,6 +99,18 @@ testSheet = function (sheet) {
|
||||
window.navigator.userAgent.indexOf('Trident/') >= 0) {
|
||||
text = ieFormat(text);
|
||||
}
|
||||
|
||||
// Normalize URLs: convert absolute URLs back to relative for comparison
|
||||
// The browser resolves relative URLs when reading from DOM, but we want to compare against the original relative URLs
|
||||
lessOutput = lessOutput.replace(/url\("http:\/\/localhost:8081\/packages\/less\/node_modules\/@less\/test-data\/tests-unit\/([^"]+)"\)/g, 'url("$1")');
|
||||
// Also normalize directory-prefixed relative URLs (e.g., "at-rules/myfont.woff2" -> "myfont.woff2")
|
||||
// This happens because the browser resolves URLs relative to the HTML document location
|
||||
lessOutput = lessOutput.replace(/url\("([a-z-]+\/)([^"]+)"\)/g, 'url("$2")');
|
||||
// Also normalize @import statements that get resolved to absolute URLs
|
||||
lessOutput = lessOutput.replace(/@import "http:\/\/localhost:8081\/packages\/less\/node_modules\/@less\/test-data\/tests-unit\/([^"]+)"(.*);/g, '@import "$1"$2;');
|
||||
// Also normalize @import with directory prefix (e.g., "at-rules-keyword-comments/test.css" -> "test.css")
|
||||
lessOutput = lessOutput.replace(/@import "([a-z-]+\/)([^"]+)"(.*);/g, '@import "$2"$3;');
|
||||
|
||||
expect(lessOutput).to.equal(text);
|
||||
done();
|
||||
})
|
||||
@@ -164,12 +176,21 @@ testErrorSheet = function (sheet) {
|
||||
.replace(/\nStack Trace\n[\s\S]*/i, '')
|
||||
.replace(/\n$/, '')
|
||||
.trim();
|
||||
actualErrorMsg = actualErrorMsg
|
||||
.replace(/ in [\w\-]+\.less( on line \d+, column \d+)?:?$/, '') // Remove filename and optional line/column from end of error message
|
||||
.replace(/\{path\}/g, '')
|
||||
.replace(/\{pathrel\}/g, '')
|
||||
.replace(/\{pathhref\}/g, 'http://localhost:8081/packages/less/node_modules/@less/test-data/tests-error/eval/')
|
||||
.replace(/\{404status\}/g, ' (404)')
|
||||
.replace(/\{node\}[\s\S]*\{\/node\}/g, '')
|
||||
.replace(/\n$/, '')
|
||||
.trim();
|
||||
errorFile
|
||||
.then(function (errorTxt) {
|
||||
errorTxt = errorTxt
|
||||
.replace(/\{path\}/g, '')
|
||||
.replace(/\{pathrel\}/g, '')
|
||||
.replace(/\{pathhref\}/g, 'http://localhost:8081/test/less/errors/')
|
||||
.replace(/\{pathhref\}/g, 'http://localhost:8081/packages/less/node_modules/@less/test-data/tests-error/eval/')
|
||||
.replace(/\{404status\}/g, ' (404)')
|
||||
.replace(/\{node\}[\s\S]*\{\/node\}/g, '')
|
||||
.replace(/\n$/, '')
|
||||
|
||||
@@ -4,21 +4,25 @@ var { forceCovertToBrowserPath } = require('./utils');
|
||||
|
||||
/** Root of repo */
|
||||
var testFolder = forceCovertToBrowserPath(path.dirname(resolve.sync('@less/test-data')));
|
||||
var lessFolder = forceCovertToBrowserPath(path.join(testFolder, 'less'));
|
||||
var testsUnitFolder = forceCovertToBrowserPath(path.join(testFolder, 'tests-unit'));
|
||||
var testsConfigFolder = forceCovertToBrowserPath(path.join(testFolder, 'tests-config'));
|
||||
var localTests = forceCovertToBrowserPath(path.resolve(__dirname, '..'));
|
||||
|
||||
module.exports = {
|
||||
main: {
|
||||
// src is used to build list of less files to compile
|
||||
src: [
|
||||
`${lessFolder}/_main/*.less`,
|
||||
`!${lessFolder}/_main/plugin-preeval.less`, // uses ES6 syntax
|
||||
`${testsUnitFolder}/*/*.less`,
|
||||
`!${testsUnitFolder}/plugin-preeval/plugin-preeval.less`, // uses ES6 syntax
|
||||
// Don't test NPM import, obviously
|
||||
`!${lessFolder}/_main/plugin-module.less`,
|
||||
`!${lessFolder}/_main/import-module.less`,
|
||||
`!${lessFolder}/_main/javascript.less`,
|
||||
`!${lessFolder}/_main/urls.less`,
|
||||
`!${lessFolder}/_main/empty.less`
|
||||
`!${testsUnitFolder}/plugin-module/plugin-module.less`,
|
||||
`!${testsUnitFolder}/import/import-module.less`,
|
||||
`!${testsUnitFolder}/javascript/javascript.less`,
|
||||
`!${testsUnitFolder}/urls/urls.less`,
|
||||
`!${testsUnitFolder}/empty/empty.less`,
|
||||
`!${testsUnitFolder}/color-functions/operations.less`, // conflicts with operations/operations.less
|
||||
// Exclude debug line numbers tests - these are Node.js only (dumpLineNumbers is deprecated)
|
||||
`!${testsConfigFolder}/debug/**/*.less`
|
||||
],
|
||||
options: {
|
||||
helpers: 'test/browser/runner-main-options.js',
|
||||
@@ -26,16 +30,8 @@ module.exports = {
|
||||
outfile: 'tmp/browser/test-runner-main.html'
|
||||
}
|
||||
},
|
||||
legacy: {
|
||||
src: [`${lessFolder}/legacy/*.less`],
|
||||
options: {
|
||||
helpers: 'test/browser/runner-legacy-options.js',
|
||||
specs: 'test/browser/runner-legacy-spec.js',
|
||||
outfile: 'tmp/browser/test-runner-legacy.html'
|
||||
}
|
||||
},
|
||||
strictUnits: {
|
||||
src: [`${lessFolder}/units/strict/*.less`],
|
||||
src: [`${testsConfigFolder}/units/strict/*.less`],
|
||||
options: {
|
||||
helpers: 'test/browser/runner-strict-units-options.js',
|
||||
specs: 'test/browser/runner-strict-units-spec.js',
|
||||
@@ -44,8 +40,8 @@ module.exports = {
|
||||
},
|
||||
errors: {
|
||||
src: [
|
||||
`${lessFolder}/errors/*.less`,
|
||||
`${testFolder}/errors/javascript-error.less`,
|
||||
`${testFolder}/tests-error/eval/*.less`,
|
||||
`${testFolder}/tests-error/parse/*.less`,
|
||||
`${localTests}/less/errors/*.less`
|
||||
],
|
||||
options: {
|
||||
@@ -56,7 +52,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
noJsErrors: {
|
||||
src: [`${lessFolder}/no-js-errors/*.less`],
|
||||
src: [`${testsConfigFolder}/no-js-errors/*.less`],
|
||||
options: {
|
||||
helpers: 'test/browser/runner-no-js-errors-options.js',
|
||||
specs: 'test/browser/runner-no-js-errors-spec.js',
|
||||
@@ -141,7 +137,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
postProcessorPlugin: {
|
||||
src: [`${lessFolder}/postProcessorPlugin/*.less`],
|
||||
src: [`${testsConfigFolder}/postProcessorPlugin/*.less`],
|
||||
options: {
|
||||
helpers: [
|
||||
'test/plugins/postprocess/index.js',
|
||||
@@ -153,7 +149,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
preProcessorPlugin: {
|
||||
src: [`${lessFolder}/preProcessorPlugin/*.less`],
|
||||
src: [`${testsConfigFolder}/preProcessorPlugin/*.less`],
|
||||
options: {
|
||||
helpers: [
|
||||
'test/plugins/preprocess/index.js',
|
||||
@@ -164,7 +160,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
visitorPlugin: {
|
||||
src: [`${lessFolder}/visitorPlugin/*.less`],
|
||||
src: [`${testsConfigFolder}/visitorPlugin/*.less`],
|
||||
options: {
|
||||
helpers: [
|
||||
'test/plugins/visitor/index.js',
|
||||
@@ -175,7 +171,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
filemanagerPlugin: {
|
||||
src: [`${lessFolder}/filemanagerPlugin/*.less`],
|
||||
src: [`${testsConfigFolder}/filemanagerPlugin/*.less`],
|
||||
options: {
|
||||
helpers: [
|
||||
'test/plugins/filemanager/index.js',
|
||||
|
||||
@@ -25,9 +25,20 @@ module.exports = (stylesheets, helpers, spec, less) => {
|
||||
<!-- for each test, generate CSS/LESS link tags -->
|
||||
$${stylesheets.map(function(fullLessName) {
|
||||
var pathParts = fullLessName.split('/');
|
||||
var fullCssName = fullLessName
|
||||
.replace(/\/(browser|test-data)\/less\//g, '/$1/css/')
|
||||
.replace(/less$/, 'css')
|
||||
var fullCssName = fullLessName.replace(/less$/, 'css');
|
||||
|
||||
// Check if the CSS file exists in the same directory as the LESS file
|
||||
var fs = require('fs');
|
||||
var cssExists = fs.existsSync(fullCssName);
|
||||
|
||||
// If not, try the css/ directory for local browser tests
|
||||
if (!cssExists && fullLessName.includes('/test/browser/less/')) {
|
||||
var cssInCssDir = fullLessName.replace('/test/browser/less/', '/test/browser/css/').replace(/less$/, 'css');
|
||||
if (fs.existsSync(cssInCssDir)) {
|
||||
fullCssName = cssInCssDir;
|
||||
}
|
||||
}
|
||||
|
||||
var lessName = pathParts[pathParts.length - 1];
|
||||
var name = lessName.split('.')[0];
|
||||
return `
|
||||
|
||||
@@ -6,7 +6,7 @@ var less = {
|
||||
};
|
||||
|
||||
// test inline less in style tags by grabbing an assortment of less files and doing `@import`s
|
||||
var testFiles = ['charsets', 'colors', 'comments', 'css-3', 'strings', 'media', 'mixins'],
|
||||
var testFiles = ['charsets/charsets', 'color-functions/basic', 'comments/comments', 'css-3/css-3', 'strings/strings', 'media/media', 'mixins/mixins'],
|
||||
testSheets = [];
|
||||
|
||||
// setup style tags with less and link tags pointing to expected css output
|
||||
@@ -14,13 +14,13 @@ var testFiles = ['charsets', 'colors', 'comments', 'css-3', 'strings', 'media',
|
||||
/**
|
||||
* @todo - generate the node_modules path for this file and in templates
|
||||
*/
|
||||
var lessFolder = '../../node_modules/@less/test-data/less'
|
||||
var cssFolder = '../../node_modules/@less/test-data/css'
|
||||
var lessFolder = '../../node_modules/@less/test-data/tests-unit'
|
||||
var cssFolder = '../../node_modules/@less/test-data/tests-unit'
|
||||
|
||||
for (var i = 0; i < testFiles.length; i++) {
|
||||
var file = testFiles[i],
|
||||
lessPath = lessFolder + '/_main/' + file + '.less',
|
||||
cssPath = cssFolder + '/_main/' + file + '.css',
|
||||
lessPath = lessFolder + '/' + file + '.less',
|
||||
cssPath = cssFolder + '/' + file + '.css',
|
||||
lessStyle = document.createElement('style'),
|
||||
cssLink = document.createElement('link'),
|
||||
lessText = '@import "' + lessPath + '";';
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
var less = {
|
||||
logLevel: 4,
|
||||
errorReporting: 'console',
|
||||
math: 'always',
|
||||
strictUnits: false
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
describe('less.js legacy tests', function() {
|
||||
testLessEqualsInDocument();
|
||||
});
|
||||
@@ -1,112 +1,307 @@
|
||||
var lessTest = require('./less-test'),
|
||||
lessTester = lessTest(),
|
||||
path = require('path'),
|
||||
stylize = require('../lib/less-node/lessc-helper').stylize,
|
||||
nock = require('nock');
|
||||
// Mock needle for HTTP requests BEFORE any other requires
|
||||
const Module = require('module');
|
||||
const originalRequire = Module.prototype.require;
|
||||
Module.prototype.require = function(id) {
|
||||
if (id === 'needle') {
|
||||
return {
|
||||
get: function(url, options, callback) {
|
||||
|
||||
// Handle CDN requests
|
||||
if (url.includes('cdn.jsdelivr.net')) {
|
||||
if (url.includes('selectors.less')) {
|
||||
setTimeout(() => {
|
||||
callback(null, { statusCode: 200 }, fs.readFileSync(path.join(__dirname, '../../test-data/tests-unit/selectors/selectors.less'), 'utf8'));
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
if (url.includes('media.less')) {
|
||||
setTimeout(() => {
|
||||
callback(null, { statusCode: 200 }, fs.readFileSync(path.join(__dirname, '../../test-data/tests-unit/media/media.less'), 'utf8'));
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
if (url.includes('empty.less')) {
|
||||
setTimeout(() => {
|
||||
callback(null, { statusCode: 200 }, fs.readFileSync(path.join(__dirname, '../../test-data/tests-unit/empty/empty.less'), 'utf8'));
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle redirect test - simulate needle's automatic redirect handling
|
||||
if (url.includes('example.com/redirect.less')) {
|
||||
setTimeout(() => {
|
||||
// Simulate the final response after needle automatically follows the redirect
|
||||
callback(null, { statusCode: 200 }, 'h1 { color: blue; }');
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.includes('example.com/target.less')) {
|
||||
setTimeout(() => {
|
||||
callback(null, { statusCode: 200 }, 'h1 { color: blue; }');
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default error for unmocked URLs
|
||||
setTimeout(() => {
|
||||
callback(new Error('Unmocked URL: ' + url), null, null);
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
}
|
||||
return originalRequire.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Now load other modules after mocking is set up
|
||||
var path = require('path'),
|
||||
fs = require('fs'),
|
||||
lessTest = require('./less-test'),
|
||||
stylize = require('../lib/less-node/lessc-helper').stylize;
|
||||
|
||||
// Parse command line arguments for test filtering
|
||||
var args = process.argv.slice(2);
|
||||
var testFilter = args.length > 0 ? args[0] : null;
|
||||
|
||||
// Create the test runner with the filter
|
||||
var lessTester = lessTest(testFilter);
|
||||
|
||||
// HTTP mocking is now handled by needle mocking above
|
||||
|
||||
// Test HTTP redirect functionality
|
||||
function testHttpRedirects() {
|
||||
const less = require('../lib/less-node').default;
|
||||
|
||||
console.log('🧪 Testing HTTP redirect functionality...');
|
||||
|
||||
const redirectTest = `
|
||||
@import "https://example.com/redirect.less";
|
||||
|
||||
h1 { color: red; }
|
||||
`;
|
||||
|
||||
return less.render(redirectTest, {
|
||||
filename: 'test-redirect.less'
|
||||
}).then(result => {
|
||||
console.log('✅ HTTP redirect test SUCCESS:');
|
||||
console.log(result.css);
|
||||
|
||||
// Check if both imported and local content are present
|
||||
if (result.css.includes('color: blue') && result.css.includes('color: red')) {
|
||||
console.log('🎉 HTTP redirect test PASSED - both imported and local content found');
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ HTTP redirect test FAILED - missing expected content');
|
||||
return false;
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log('❌ HTTP redirect test ERROR:');
|
||||
console.log(err.message);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Test import-remote functionality
|
||||
function testImportRemote() {
|
||||
const less = require('../lib/less-node').default;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('🧪 Testing import-remote functionality...');
|
||||
|
||||
const testFile = path.join(__dirname, '../../test-data/tests-unit/import/import-remote.less');
|
||||
const expectedFile = path.join(__dirname, '../../test-data/tests-unit/import/import-remote.css');
|
||||
|
||||
const content = fs.readFileSync(testFile, 'utf8');
|
||||
const expected = fs.readFileSync(expectedFile, 'utf8');
|
||||
|
||||
return less.render(content, {
|
||||
filename: testFile
|
||||
}).then(result => {
|
||||
console.log('✅ Import-remote test SUCCESS:');
|
||||
console.log('Expected:', expected.trim());
|
||||
console.log('Actual:', result.css.trim());
|
||||
|
||||
if (result.css.trim() === expected.trim()) {
|
||||
console.log('🎉 Import-remote test PASSED - CDN imports and variable resolution working');
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ Import-remote test FAILED - output mismatch');
|
||||
return false;
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log('❌ Import-remote test ERROR:');
|
||||
console.log(err.message);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n' + stylize('Less', 'underline') + '\n');
|
||||
|
||||
if (testFilter) {
|
||||
console.log('Running tests matching: ' + testFilter + '\n');
|
||||
}
|
||||
|
||||
// Glob patterns for main test runs (excluding problematic tests that will run separately)
|
||||
var globPatterns = [
|
||||
'tests-config/*/*.less',
|
||||
'tests-unit/*/*.less',
|
||||
// Debug tests have nested subdirectories (comments/, mediaquery/, all/)
|
||||
'tests-config/debug/*/linenumbers-*.less',
|
||||
'!tests-config/sourcemaps/**/*.less', // Exclude sourcemaps (need special handling)
|
||||
'!tests-config/sourcemaps-empty/*', // Exclude sourcemaps-empty (need special handling)
|
||||
'!tests-config/sourcemaps-disable-annotation/*', // Exclude sourcemaps-disable-annotation (need special handling)
|
||||
'!tests-config/sourcemaps-variable-selector/*', // Exclude sourcemaps-variable-selector (need special handling)
|
||||
'!tests-config/globalVars/*', // Exclude globalVars (need JSON config handling)
|
||||
'!tests-config/modifyVars/*', // Exclude modifyVars (need JSON config handling)
|
||||
'!tests-config/js-type-errors/*', // Exclude js-type-errors (need special test function)
|
||||
'!tests-config/no-js-errors/*', // Exclude no-js-errors (need special test function)
|
||||
'!tests-unit/import/import-remote.less', // Exclude import-remote (tested separately in isolation)
|
||||
|
||||
// HTTP import tests are now included since we have needle mocking
|
||||
];
|
||||
|
||||
var testMap = [
|
||||
[{
|
||||
// TODO: Change this to rewriteUrls: 'all' once the relativeUrls option is removed
|
||||
relativeUrls: true,
|
||||
silent: true,
|
||||
javascriptEnabled: true
|
||||
}, '_main/'],
|
||||
[{}, 'namespacing/'],
|
||||
[{
|
||||
math: 'parens'
|
||||
}, 'math/strict/'],
|
||||
[{
|
||||
math: 'parens-division'
|
||||
}, 'math/parens-division/'],
|
||||
[{
|
||||
math: 'always'
|
||||
}, 'math/always/'],
|
||||
// Use legacy strictMath: true here to demonstrate it still works
|
||||
[{strictMath: true, strictUnits: true, javascriptEnabled: true}, '../errors/eval/',
|
||||
lessTester.testErrors, null],
|
||||
[{strictMath: true, strictUnits: true, javascriptEnabled: true}, '../errors/parse/',
|
||||
lessTester.testErrors, null],
|
||||
[{math: 'strict', strictUnits: true, javascriptEnabled: true}, 'js-type-errors/',
|
||||
lessTester.testTypeErrors, null],
|
||||
[{math: 'strict', strictUnits: true, javascriptEnabled: false}, 'no-js-errors/',
|
||||
lessTester.testErrors, null],
|
||||
[{math: 'strict', dumpLineNumbers: 'comments'}, 'debug/', null,
|
||||
function(name) { return name + '-comments'; }],
|
||||
[{math: 'strict', dumpLineNumbers: 'mediaquery'}, 'debug/', null,
|
||||
function(name) { return name + '-mediaquery'; }],
|
||||
[{math: 'strict', dumpLineNumbers: 'all'}, 'debug/', null,
|
||||
function(name) { return name + '-all'; }],
|
||||
// TODO: Change this to rewriteUrls: false once the relativeUrls option is removed
|
||||
[{math: 'strict', relativeUrls: false, rootpath: 'folder (1)/'}, 'static-urls/'],
|
||||
[{math: 'strict', compress: true}, 'compression/'],
|
||||
// Main test runs using glob patterns (cosmiconfig handles configs)
|
||||
{
|
||||
patterns: globPatterns
|
||||
},
|
||||
|
||||
[{math: 0, strictUnits: true}, 'units/strict/'],
|
||||
[{math: 0, strictUnits: false}, 'units/no-strict/'],
|
||||
// Error tests
|
||||
{
|
||||
patterns: ['tests-error/eval/*.less'],
|
||||
verifyFunction: lessTester.testErrors
|
||||
},
|
||||
{
|
||||
patterns: ['tests-error/parse/*.less'],
|
||||
verifyFunction: lessTester.testErrors
|
||||
},
|
||||
|
||||
[{math: 'strict', strictUnits: true, sourceMap: true, globalVars: true }, 'sourcemaps/',
|
||||
lessTester.testSourcemap, null, null,
|
||||
function(filename, type, baseFolder) {
|
||||
// Special test cases with specific handling
|
||||
{
|
||||
patterns: ['tests-config/js-type-errors/*.less'],
|
||||
verifyFunction: lessTester.testTypeErrors
|
||||
},
|
||||
{
|
||||
patterns: ['tests-config/no-js-errors/*.less'],
|
||||
verifyFunction: lessTester.testErrors
|
||||
},
|
||||
|
||||
// Sourcemap tests with special handling
|
||||
{
|
||||
patterns: [
|
||||
'tests-config/sourcemaps/**/*.less',
|
||||
'tests-config/sourcemaps-url/**/*.less',
|
||||
'tests-config/sourcemaps-rootpath/**/*.less',
|
||||
'tests-config/sourcemaps-basepath/**/*.less',
|
||||
'tests-config/sourcemaps-include-source/**/*.less'
|
||||
],
|
||||
verifyFunction: lessTester.testSourcemap,
|
||||
getFilename: function(filename, type, baseFolder) {
|
||||
if (type === 'vars') {
|
||||
return path.join(baseFolder, filename) + '.json';
|
||||
}
|
||||
return path.join('test/sourcemaps', filename) + '.json';
|
||||
}],
|
||||
// Extract just the filename (without directory) for the JSON file
|
||||
var jsonFilename = path.basename(filename);
|
||||
// For sourcemap type, return path relative to test directory
|
||||
if (type === 'sourcemap') {
|
||||
return path.join('test/sourcemaps', jsonFilename) + '.json';
|
||||
}
|
||||
return path.join('test/sourcemaps', jsonFilename) + '.json';
|
||||
}
|
||||
},
|
||||
{
|
||||
patterns: ['tests-config/sourcemaps-empty/*.less'],
|
||||
verifyFunction: lessTester.testEmptySourcemap
|
||||
},
|
||||
{
|
||||
patterns: ['tests-config/sourcemaps-disable-annotation/*.less'],
|
||||
verifyFunction: lessTester.testSourcemapWithoutUrlAnnotation
|
||||
},
|
||||
{
|
||||
patterns: ['tests-config/sourcemaps-variable-selector/*.less'],
|
||||
verifyFunction: lessTester.testSourcemapWithVariableInSelector
|
||||
},
|
||||
|
||||
[{math: 'strict', strictUnits: true, globalVars: true }, '_main/import/json/',
|
||||
lessTester.testImports, null, true,
|
||||
function(filename, type, baseFolder) {
|
||||
return path.join(baseFolder, filename) + '.json';
|
||||
}],
|
||||
[{math: 'strict', strictUnits: true, sourceMap: {sourceMapFileInline: true}},
|
||||
'sourcemaps-empty/', lessTester.testEmptySourcemap],
|
||||
[{math: 'strict', strictUnits: true, sourceMap: {disableSourcemapAnnotation: true}},
|
||||
'sourcemaps-disable-annotation/', lessTester.testSourcemapWithoutUrlAnnotation],
|
||||
[{math: 'strict', strictUnits: true, sourceMap: true},
|
||||
'sourcemaps-variable-selector/', lessTester.testSourcemapWithVariableInSelector],
|
||||
[{globalVars: true, banner: '/**\n * Test\n */\n'}, 'globalVars/',
|
||||
null, null, null, function(name, type, baseFolder) { return path.join(baseFolder, name) + '.json'; }],
|
||||
[{modifyVars: true}, 'modifyVars/',
|
||||
null, null, null, function(name, type, baseFolder) { return path.join(baseFolder, name) + '.json'; }],
|
||||
[{urlArgs: '424242'}, 'url-args/'],
|
||||
[{rewriteUrls: 'all'}, 'rewrite-urls-all/'],
|
||||
[{rewriteUrls: 'local'}, 'rewrite-urls-local/'],
|
||||
[{rootpath: 'http://example.com/assets/css/', rewriteUrls: 'all'}, 'rootpath-rewrite-urls-all/'],
|
||||
[{rootpath: 'http://example.com/assets/css/', rewriteUrls: 'local'}, 'rootpath-rewrite-urls-local/'],
|
||||
[{paths: ['data/', '_main/import/']}, 'include-path/'],
|
||||
[{paths: 'data/'}, 'include-path-string/'],
|
||||
[{plugin: 'test/plugins/postprocess/'}, 'postProcessorPlugin/'],
|
||||
[{plugin: 'test/plugins/preprocess/'}, 'preProcessorPlugin/'],
|
||||
[{plugin: 'test/plugins/visitor/'}, 'visitorPlugin/'],
|
||||
[{plugin: 'test/plugins/filemanager/'}, 'filemanagerPlugin/'],
|
||||
[{math: 0}, '3rd-party/'],
|
||||
[{ processImports: false }, 'process-imports/']
|
||||
// Import tests with JSON configs
|
||||
{
|
||||
patterns: ['tests-config/globalVars/*.less'],
|
||||
lessOptions: {
|
||||
globalVars: function(file) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const basename = path.basename(file, '.less');
|
||||
const jsonPath = path.join(path.dirname(file), basename + '.json');
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
patterns: ['tests-config/modifyVars/*.less'],
|
||||
lessOptions: {
|
||||
modifyVars: function(file) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const basename = path.basename(file, '.less');
|
||||
const jsonPath = path.join(path.dirname(file), basename + '.json');
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
testMap.forEach(function(args) {
|
||||
lessTester.runTestSet.apply(lessTester, args)
|
||||
|
||||
// Note: needle mocking is set up globally at the top of the file
|
||||
|
||||
testMap.forEach(function(testConfig) {
|
||||
// For glob patterns, pass lessOptions as the first parameter and patterns as the second
|
||||
if (testConfig.patterns) {
|
||||
lessTester.runTestSet(
|
||||
testConfig.lessOptions || {}, // First param: options (including lessOptions)
|
||||
testConfig.patterns, // Second param: patterns
|
||||
testConfig.verifyFunction || null, // Third param: verifyFunction
|
||||
testConfig.nameModifier || null, // Fourth param: nameModifier
|
||||
testConfig.doReplacements || null, // Fifth param: doReplacements
|
||||
testConfig.getFilename || null // Sixth param: getFilename
|
||||
);
|
||||
} else {
|
||||
// Legacy format for non-glob tests
|
||||
var args = [
|
||||
testConfig.options || {}, // First param: options
|
||||
testConfig.foldername, // Second param: foldername
|
||||
testConfig.verifyFunction || null, // Third param: verifyFunction
|
||||
testConfig.nameModifier || null, // Fourth param: nameModifier
|
||||
testConfig.doReplacements || null, // Fifth param: doReplacements
|
||||
testConfig.getFilename || null // Sixth param: getFilename
|
||||
];
|
||||
lessTester.runTestSet.apply(lessTester, args);
|
||||
}
|
||||
});
|
||||
lessTester.testSyncronous({syncImport: true}, '_main/import');
|
||||
lessTester.testSyncronous({syncImport: true}, '_main/plugin');
|
||||
lessTester.testSyncronous({syncImport: true}, 'math/strict/css');
|
||||
|
||||
// Special synchronous tests
|
||||
lessTester.testSyncronous({syncImport: true}, 'tests-unit/import/import');
|
||||
lessTester.testSyncronous({syncImport: true}, 'tests-config/math-strict/css');
|
||||
|
||||
lessTester.testNoOptions();
|
||||
lessTester.testDisablePluginRule();
|
||||
lessTester.testJSImport();
|
||||
lessTester.finished();
|
||||
|
||||
(() => {
|
||||
// Create new tester, since tests are not independent and tests
|
||||
// above modify tester in a way that breaks remote imports.
|
||||
lessTester = lessTest();
|
||||
var scope = nock('https://example.com')
|
||||
.get('/redirect.less').query(true)
|
||||
.reply(301, null, { location: '/target.less' })
|
||||
.get('/target.less').query(true)
|
||||
.reply(200);
|
||||
lessTester.runTestSet(
|
||||
{},
|
||||
'import-redirect/',
|
||||
lessTester.testImportRedirect(scope)
|
||||
);
|
||||
lessTester.finished();
|
||||
})();
|
||||
|
||||
// Test HTTP redirect functionality
|
||||
console.log('\nTesting HTTP redirect functionality...');
|
||||
testHttpRedirects();
|
||||
console.log('HTTP redirect test completed');
|
||||
|
||||
// Test import-remote functionality in isolation
|
||||
console.log('\nTesting import-remote functionality...');
|
||||
testImportRemote();
|
||||
console.log('Import-remote test completed');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* jshint latedef: nofunc */
|
||||
var semver = require('semver');
|
||||
var logger = require('../lib/less/logger').default;
|
||||
var { cosmiconfigSync } = require('cosmiconfig');
|
||||
var glob = require('glob');
|
||||
|
||||
var isVerbose = process.env.npm_config_loglevel !== 'concise';
|
||||
logger.addListener({
|
||||
@@ -18,7 +20,7 @@ logger.addListener({
|
||||
});
|
||||
|
||||
|
||||
module.exports = function() {
|
||||
module.exports = function(testFilter) {
|
||||
var path = require('path'),
|
||||
fs = require('fs'),
|
||||
clone = require('copy-anything').copy;
|
||||
@@ -29,11 +31,11 @@ module.exports = function() {
|
||||
|
||||
var globals = Object.keys(global);
|
||||
|
||||
var oneTestOnly = process.argv[2],
|
||||
var oneTestOnly = testFilter || process.argv[2],
|
||||
isFinished = false;
|
||||
|
||||
var testFolder = path.dirname(require.resolve('@less/test-data'));
|
||||
var lessFolder = path.join(testFolder, 'less');
|
||||
var lessFolder = testFolder;
|
||||
|
||||
// Define String.prototype.endsWith if it doesn't exist (in older versions of node)
|
||||
// This is required by the testSourceMap function below
|
||||
@@ -83,33 +85,202 @@ module.exports = function() {
|
||||
}
|
||||
});
|
||||
|
||||
function testSourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder) {
|
||||
function validateSourcemapMappings(sourcemap, lessFile, compiledCSS) {
|
||||
// Validate sourcemap mappings using SourceMapConsumer
|
||||
var SourceMapConsumer = require('source-map').SourceMapConsumer;
|
||||
// sourcemap can be either a string or already parsed object
|
||||
var sourceMapObj = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap;
|
||||
var consumer = new SourceMapConsumer(sourceMapObj);
|
||||
|
||||
// Read the LESS source file
|
||||
var lessSource = fs.readFileSync(lessFile, 'utf8');
|
||||
var lessLines = lessSource.split('\n');
|
||||
|
||||
// Use the compiled CSS (remove sourcemap annotation for validation)
|
||||
var cssSource = compiledCSS.replace(/\/\*# sourceMappingURL=.*\*\/\s*$/, '').trim();
|
||||
var cssLines = cssSource.split('\n');
|
||||
|
||||
var errors = [];
|
||||
var validatedMappings = 0;
|
||||
|
||||
// Validate mappings for each line in the CSS
|
||||
for (var cssLine = 1; cssLine <= cssLines.length; cssLine++) {
|
||||
var cssLineContent = cssLines[cssLine - 1];
|
||||
// Skip empty lines
|
||||
if (!cssLineContent.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check mapping for the start of this CSS line
|
||||
var mapping = consumer.originalPositionFor({
|
||||
line: cssLine,
|
||||
column: 0
|
||||
});
|
||||
|
||||
if (mapping.source) {
|
||||
validatedMappings++;
|
||||
|
||||
// Verify the source file exists in the sourcemap
|
||||
if (!sourceMapObj.sources || sourceMapObj.sources.indexOf(mapping.source) === -1) {
|
||||
errors.push('Line ' + cssLine + ': mapped to source "' + mapping.source + '" which is not in sources array');
|
||||
}
|
||||
|
||||
// Verify the line number is valid
|
||||
if (mapping.line && mapping.line > 0) {
|
||||
// If we can find the source file, validate the line exists
|
||||
var sourceIndex = sourceMapObj.sources.indexOf(mapping.source);
|
||||
if (sourceIndex >= 0 && sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[sourceIndex] !== undefined && sourceMapObj.sourcesContent[sourceIndex] !== null) {
|
||||
var sourceContent = sourceMapObj.sourcesContent[sourceIndex];
|
||||
// Ensure sourceContent is a string (it should be, but be defensive)
|
||||
if (typeof sourceContent !== 'string') {
|
||||
sourceContent = String(sourceContent);
|
||||
}
|
||||
// Split by newline - handle both \n and \r\n
|
||||
var sourceLines = sourceContent.split(/\r?\n/);
|
||||
if (mapping.line > sourceLines.length) {
|
||||
errors.push('Line ' + cssLine + ': mapped to line ' + mapping.line + ' in "' + mapping.source + '" but source only has ' + sourceLines.length + ' lines');
|
||||
}
|
||||
} else if (sourceIndex >= 0) {
|
||||
// Source content not embedded, try to validate against the actual file if it matches
|
||||
// This is a best-effort validation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that all sources in the sourcemap are valid
|
||||
if (sourceMapObj.sources) {
|
||||
sourceMapObj.sources.forEach(function(source, index) {
|
||||
if (sourceMapObj.sourcesContent && sourceMapObj.sourcesContent[index]) {
|
||||
// Source content is embedded, validate it's not empty
|
||||
if (!sourceMapObj.sourcesContent[index].trim()) {
|
||||
errors.push('Source "' + source + '" has empty content');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (consumer.destroy && typeof consumer.destroy === 'function') {
|
||||
consumer.destroy();
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors: errors,
|
||||
mappingsValidated: validatedMappings
|
||||
};
|
||||
}
|
||||
|
||||
function testSourcemap(name, err, compiledLess, doReplacements, sourcemap, baseFolder, getFilename) {
|
||||
if (err) {
|
||||
fail('ERROR: ' + (err && err.message));
|
||||
return;
|
||||
}
|
||||
// Check the sourceMappingURL at the bottom of the file
|
||||
var expectedSourceMapURL = name + '.css.map',
|
||||
sourceMappingPrefix = '/*# sourceMappingURL=',
|
||||
sourceMappingSuffix = ' */',
|
||||
expectedCSSAppendage = sourceMappingPrefix + expectedSourceMapURL + sourceMappingSuffix;
|
||||
if (!compiledLess.endsWith(expectedCSSAppendage)) {
|
||||
// To display a better error message, we need to figure out what the actual sourceMappingURL value was, if it was even present
|
||||
var indexOfSourceMappingPrefix = compiledLess.indexOf(sourceMappingPrefix);
|
||||
if (indexOfSourceMappingPrefix === -1) {
|
||||
fail('ERROR: sourceMappingURL was not found in ' + baseFolder + '/' + name + '.css.');
|
||||
return;
|
||||
}
|
||||
|
||||
var startOfSourceMappingValue = indexOfSourceMappingPrefix + sourceMappingPrefix.length,
|
||||
indexOfNextSpace = compiledLess.indexOf(' ', startOfSourceMappingValue),
|
||||
actualSourceMapURL = compiledLess.substring(startOfSourceMappingValue, indexOfNextSpace === -1 ? compiledLess.length : indexOfNextSpace);
|
||||
fail('ERROR: sourceMappingURL should be "' + expectedSourceMapURL + '" but is "' + actualSourceMapURL + '".');
|
||||
// Default expected URL is name + '.css.map', but can be overridden by sourceMapURL option
|
||||
var sourceMappingPrefix = '/*# sourceMappingURL=',
|
||||
sourceMappingSuffix = ' */';
|
||||
var indexOfSourceMappingPrefix = compiledLess.indexOf(sourceMappingPrefix);
|
||||
if (indexOfSourceMappingPrefix === -1) {
|
||||
fail('ERROR: sourceMappingURL was not found in ' + baseFolder + '/' + name + '.css.');
|
||||
return;
|
||||
}
|
||||
|
||||
var startOfSourceMappingValue = indexOfSourceMappingPrefix + sourceMappingPrefix.length,
|
||||
indexOfSuffix = compiledLess.indexOf(sourceMappingSuffix, startOfSourceMappingValue),
|
||||
actualSourceMapURL = compiledLess.substring(startOfSourceMappingValue, indexOfSuffix === -1 ? compiledLess.length : indexOfSuffix).trim();
|
||||
|
||||
// For tests with custom sourceMapURL, we just verify it exists and is non-empty
|
||||
// The actual value will be validated by comparing the sourcemap JSON
|
||||
if (!actualSourceMapURL) {
|
||||
fail('ERROR: sourceMappingURL is empty in ' + baseFolder + '/' + name + '.css.');
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readFile(path.join('test/', name) + '.json', 'utf8', function (e, expectedSourcemap) {
|
||||
// Use getFilename if available (for sourcemap tests with subdirectories)
|
||||
var jsonPath;
|
||||
if (getFilename && typeof getFilename === 'function') {
|
||||
jsonPath = getFilename(name, 'sourcemap', baseFolder);
|
||||
} else {
|
||||
// Fallback: extract just the filename for sourcemap JSON files
|
||||
var jsonFilename = path.basename(name);
|
||||
jsonPath = path.join('test/sourcemaps', jsonFilename) + '.json';
|
||||
}
|
||||
fs.readFile(jsonPath, 'utf8', function (e, expectedSourcemap) {
|
||||
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
||||
if (sourcemap === expectedSourcemap) {
|
||||
if (e) {
|
||||
fail('ERROR: Could not read expected sourcemap file: ' + jsonPath + ' - ' + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply doReplacements to the expected sourcemap to handle {path} placeholders
|
||||
// This normalizes absolute paths that differ between environments
|
||||
// For sourcemaps, we need to ensure {path} uses forward slashes to avoid breaking JSON
|
||||
// (backslashes in JSON strings need escaping, and sourcemaps should use forward slashes anyway)
|
||||
var replacementPath = path.join(path.dirname(path.join(baseFolder, name) + '.less'), '/');
|
||||
// Normalize to forward slashes for sourcemap JSON (web-compatible)
|
||||
replacementPath = replacementPath.replace(/\\/g, '/');
|
||||
// Replace {path} with normalized forward-slash path BEFORE calling doReplacements
|
||||
// This ensures the JSON is always valid and uses web-compatible paths
|
||||
expectedSourcemap = expectedSourcemap.replace(/\{path\}/g, replacementPath);
|
||||
// Also handle other placeholders that might be in the sourcemap (but {path} is already done)
|
||||
expectedSourcemap = doReplacements(expectedSourcemap, baseFolder, path.join(baseFolder, name) + '.less');
|
||||
|
||||
// Normalize paths in sourcemap JSON to use forward slashes (web-compatible)
|
||||
// We need to parse the JSON, normalize the file property, then stringify for comparison
|
||||
// This avoids breaking escape sequences like \n in the JSON string
|
||||
function normalizeSourcemapPaths(sm) {
|
||||
try {
|
||||
var parsed = typeof sm === 'string' ? JSON.parse(sm) : sm;
|
||||
if (parsed.file) {
|
||||
parsed.file = parsed.file.replace(/\\/g, '/');
|
||||
}
|
||||
// Also normalize paths in sources array
|
||||
if (parsed.sources && Array.isArray(parsed.sources)) {
|
||||
parsed.sources = parsed.sources.map(function(src) {
|
||||
return src.replace(/\\/g, '/');
|
||||
});
|
||||
}
|
||||
return JSON.stringify(parsed, null, 0);
|
||||
} catch (parseErr) {
|
||||
// If parsing fails, return original (shouldn't happen)
|
||||
return sm;
|
||||
}
|
||||
}
|
||||
|
||||
var normalizedSourcemap = normalizeSourcemapPaths(sourcemap);
|
||||
var normalizedExpected = normalizeSourcemapPaths(expectedSourcemap);
|
||||
|
||||
if (normalizedSourcemap === normalizedExpected) {
|
||||
// Validate the sourcemap mappings are correct
|
||||
// Find the actual LESS file - it might be in a subdirectory
|
||||
var nameParts = name.split('/');
|
||||
var lessFileName = nameParts[nameParts.length - 1];
|
||||
var lessFileDir = nameParts.length > 1 ? nameParts.slice(0, -1).join('/') : '';
|
||||
var lessFile = path.join(lessFolder, lessFileDir, lessFileName) + '.less';
|
||||
|
||||
// Only validate if the LESS file exists
|
||||
if (fs.existsSync(lessFile)) {
|
||||
try {
|
||||
// Parse the sourcemap once for validation (avoid re-parsing)
|
||||
// Use the original sourcemap string, not the normalized one
|
||||
var sourceMapObjForValidation = typeof sourcemap === 'string' ? JSON.parse(sourcemap) : sourcemap;
|
||||
var validation = validateSourcemapMappings(sourceMapObjForValidation, lessFile, compiledLess);
|
||||
if (!validation.valid) {
|
||||
fail('ERROR: Sourcemap validation failed:\n' + validation.errors.join('\n'));
|
||||
return;
|
||||
}
|
||||
if (isVerbose && validation.mappingsValidated > 0) {
|
||||
process.stdout.write(' (validated ' + validation.mappingsValidated + ' mappings)');
|
||||
}
|
||||
} catch (validationErr) {
|
||||
if (isVerbose) {
|
||||
process.stdout.write(' (validation error: ' + validationErr.message + ')');
|
||||
}
|
||||
// Don't fail the test if validation has an error, just log it
|
||||
}
|
||||
}
|
||||
|
||||
ok('OK');
|
||||
} else if (err) {
|
||||
fail('ERROR: ' + (err && err.message));
|
||||
@@ -118,7 +289,7 @@ module.exports = function() {
|
||||
process.stdout.write(err.stack + '\n');
|
||||
}
|
||||
} else {
|
||||
difference('FAIL', expectedSourcemap, sourcemap);
|
||||
difference('FAIL', normalizedExpected, normalizedSourcemap);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -281,7 +452,7 @@ module.exports = function() {
|
||||
return new less.tree.Anonymous('file');
|
||||
});
|
||||
var expected = '@charset "utf-8";\n';
|
||||
toCSS({}, path.join(lessFolder, 'root-registry', 'root.less'), function(error, output) {
|
||||
toCSS({}, path.join(lessFolder, 'tests-config', 'root-registry', 'root.less'), function(error, output) {
|
||||
if (error) {
|
||||
return fail('ERROR: ' + error);
|
||||
}
|
||||
@@ -294,9 +465,42 @@ module.exports = function() {
|
||||
|
||||
function globalReplacements(input, directory, filename) {
|
||||
var path = require('path');
|
||||
var p = filename ? path.join(path.dirname(filename), '/') : directory,
|
||||
pathimport = path.join(directory + 'import/'),
|
||||
pathesc = p.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }),
|
||||
var p = filename ? path.join(path.dirname(filename), '/') : directory;
|
||||
|
||||
// For debug tests in subdirectories (comments/, mediaquery/, all/),
|
||||
// the import/ directory and main linenumbers.less file are at the parent debug/ level, not in the subdirectory
|
||||
var isDebugSubdirectory = false;
|
||||
var debugParentPath = null;
|
||||
|
||||
if (directory) {
|
||||
// Normalize directory path separators for matching
|
||||
var normalizedDir = directory.replace(/\\/g, '/');
|
||||
// Check if we're in a debug subdirectory
|
||||
if (normalizedDir.includes('/debug/') && (normalizedDir.includes('/comments/') || normalizedDir.includes('/mediaquery/') || normalizedDir.includes('/all/'))) {
|
||||
isDebugSubdirectory = true;
|
||||
// Extract the debug/ directory path (parent of the subdirectory)
|
||||
// Match everything up to and including /debug/ (works with both absolute and relative paths)
|
||||
var debugMatch = normalizedDir.match(/(.+\/debug)\//);
|
||||
if (debugMatch) {
|
||||
debugParentPath = debugMatch[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDebugSubdirectory && debugParentPath) {
|
||||
// For {path} placeholder, use the parent debug/ directory
|
||||
// Convert back to native path format
|
||||
p = debugParentPath.replace(/\//g, path.sep) + path.sep;
|
||||
}
|
||||
|
||||
var pathimport;
|
||||
if (isDebugSubdirectory && debugParentPath) {
|
||||
pathimport = path.join(debugParentPath.replace(/\//g, path.sep), 'import') + path.sep;
|
||||
} else {
|
||||
pathimport = path.join(directory + 'import/');
|
||||
}
|
||||
|
||||
var pathesc = p.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); }),
|
||||
pathimportesc = pathimport.replace(/[.:/\\]/g, function(a) { return '\\' + (a == '\\' ? '\/' : a); });
|
||||
|
||||
return input.replace(/\{path\}/g, p)
|
||||
@@ -340,7 +544,18 @@ module.exports = function() {
|
||||
}
|
||||
|
||||
function runTestSet(options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) {
|
||||
options = options ? clone(options) : {};
|
||||
// Handle case where first parameter is glob patterns (no options object)
|
||||
if (Array.isArray(options)) {
|
||||
// First parameter is glob patterns, no options object
|
||||
foldername = options;
|
||||
options = {};
|
||||
} else if (typeof options === 'string') {
|
||||
// First parameter is foldername (no options object)
|
||||
foldername = options;
|
||||
options = {};
|
||||
} else {
|
||||
options = options ? clone(options) : {};
|
||||
}
|
||||
runTestSetInternal(lessFolder, options, foldername, verifyFunction, nameModifier, doReplacements, getFilename);
|
||||
}
|
||||
|
||||
@@ -357,41 +572,125 @@ module.exports = function() {
|
||||
doReplacements = globalReplacements;
|
||||
}
|
||||
|
||||
function getBasename(file) {
|
||||
return foldername + path.basename(file, '.less');
|
||||
// Handle glob patterns with exclusions
|
||||
if (Array.isArray(foldername)) {
|
||||
var patterns = foldername;
|
||||
var includePatterns = [];
|
||||
var excludePatterns = [];
|
||||
|
||||
|
||||
patterns.forEach(function(pattern) {
|
||||
if (pattern.startsWith('!')) {
|
||||
excludePatterns.push(pattern.substring(1));
|
||||
} else {
|
||||
includePatterns.push(pattern);
|
||||
}
|
||||
});
|
||||
|
||||
// Use glob to find all matching files, excluding the excluded patterns
|
||||
var allFiles = [];
|
||||
includePatterns.forEach(function(pattern) {
|
||||
var files = glob.sync(pattern, {
|
||||
cwd: baseFolder,
|
||||
absolute: true,
|
||||
ignore: excludePatterns
|
||||
});
|
||||
|
||||
allFiles = allFiles.concat(files);
|
||||
});
|
||||
|
||||
// Note: needle mocking is set up globally in index.js
|
||||
|
||||
// Process each .less file found
|
||||
allFiles.forEach(function(filePath) {
|
||||
if (/\.less$/.test(filePath)) {
|
||||
var file = path.basename(filePath);
|
||||
// For glob patterns, we need to construct the relative path differently
|
||||
// The filePath is absolute, so we need to get the path relative to the test-data directory
|
||||
var relativePath = path.relative(baseFolder, path.dirname(filePath)) + '/';
|
||||
|
||||
// Only process files that have corresponding .css files (these are the actual tests)
|
||||
var cssPath = path.join(path.dirname(filePath), path.basename(file, '.less') + '.css');
|
||||
if (fs.existsSync(cssPath)) {
|
||||
// Process this file using the existing logic
|
||||
processFileWithInfo({
|
||||
file: file,
|
||||
fullPath: filePath,
|
||||
relativePath: relativePath
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readdirSync(path.join(baseFolder, foldername)).forEach(function (file) {
|
||||
if (!/\.less$/.test(file)) { return; }
|
||||
function processFileWithInfo(fileInfo) {
|
||||
var file = fileInfo.file;
|
||||
var fullPath = fileInfo.fullPath;
|
||||
var relativePath = fileInfo.relativePath;
|
||||
|
||||
// Load config for this specific file using cosmiconfig
|
||||
var configResult = cosmiconfigSync('styles').search(path.dirname(fullPath));
|
||||
|
||||
// Deep clone the original options to prevent Less from modifying shared objects
|
||||
var options = JSON.parse(JSON.stringify(originalOptions || {}));
|
||||
|
||||
if (configResult && configResult.config && configResult.config.language && configResult.config.language.less) {
|
||||
// Deep clone and merge the language.less settings with the original options
|
||||
var lessConfig = JSON.parse(JSON.stringify(configResult.config.language.less));
|
||||
Object.keys(lessConfig).forEach(function(key) {
|
||||
options[key] = lessConfig[key];
|
||||
});
|
||||
}
|
||||
|
||||
// Merge any lessOptions from the testMap (for dynamic options like getVars functions)
|
||||
if (originalOptions && originalOptions.lessOptions) {
|
||||
Object.keys(originalOptions.lessOptions).forEach(function(key) {
|
||||
var value = originalOptions.lessOptions[key];
|
||||
if (typeof value === 'function') {
|
||||
// For functions, call them with the file path
|
||||
var result = value(fullPath);
|
||||
options[key] = result;
|
||||
} else {
|
||||
// For static values, use them directly
|
||||
options[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var options = clone(originalOptions);
|
||||
// Don't pass stylize to less.render as it's not a valid option
|
||||
|
||||
options.stylize = stylize;
|
||||
var name = getBasename(file, relativePath);
|
||||
|
||||
|
||||
var name = getBasename(file);
|
||||
|
||||
if (oneTestOnly && name !== oneTestOnly) {
|
||||
if (oneTestOnly && typeof oneTestOnly === 'string' && !name.includes(oneTestOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
totalTests++;
|
||||
|
||||
if (options.sourceMap && !options.sourceMap.sourceMapFileInline) {
|
||||
options.sourceMap = {
|
||||
sourceMapOutputFilename: name + '.css',
|
||||
sourceMapBasepath: baseFolder,
|
||||
sourceMapRootpath: 'testweb/',
|
||||
disableSourcemapAnnotation: options.sourceMap.disableSourcemapAnnotation
|
||||
};
|
||||
// This options is normally set by the bin/lessc script. Setting it causes the sourceMappingURL comment to be appended to the CSS
|
||||
// output. The value is designed to allow the sourceMapBasepath option to be tested, as it should be removed by less before
|
||||
// setting the sourceMappingURL value, leaving just the sourceMapOutputFilename and .map extension.
|
||||
options.sourceMap.sourceMapFilename = options.sourceMap.sourceMapBasepath + '/' + options.sourceMap.sourceMapOutputFilename + '.map';
|
||||
// Set test infrastructure defaults only if not already set by styles.config.cjs
|
||||
// Less.js core (parse-tree.js) will handle normalization of:
|
||||
// - sourceMapBasepath (defaults to input file's directory)
|
||||
// - sourceMapInputFilename (defaults to options.filename)
|
||||
// - sourceMapFilename (derived from sourceMapOutputFilename or input filename)
|
||||
// - sourceMapOutputFilename (derived from input filename if not set)
|
||||
if (!options.sourceMap.sourceMapOutputFilename) {
|
||||
// Needed for sourcemap file name in JSON output
|
||||
options.sourceMap.sourceMapOutputFilename = name + '.css';
|
||||
}
|
||||
if (!options.sourceMap.sourceMapRootpath) {
|
||||
// Test-specific default for consistent test output paths
|
||||
options.sourceMap.sourceMapRootpath = 'testweb/';
|
||||
}
|
||||
}
|
||||
|
||||
options.getVars = function(file) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(getFilename(getBasename(file), 'vars', baseFolder), 'utf8'));
|
||||
return JSON.parse(fs.readFileSync(getFilename(getBasename(file, relativePath), 'vars', baseFolder), 'utf8'));
|
||||
}
|
||||
catch (e) {
|
||||
return {};
|
||||
@@ -400,7 +699,7 @@ module.exports = function() {
|
||||
|
||||
var doubleCallCheck = false;
|
||||
queue(function() {
|
||||
toCSS(options, path.join(baseFolder, foldername + file), function (err, result) {
|
||||
toCSS(options, fullPath, function (err, result) {
|
||||
|
||||
if (doubleCallCheck) {
|
||||
totalTests++;
|
||||
@@ -416,7 +715,7 @@ module.exports = function() {
|
||||
*/
|
||||
if (verifyFunction) {
|
||||
var verificationResult = verifyFunction(
|
||||
name, err, result && result.css, doReplacements, result && result.map, baseFolder, result && result.imports
|
||||
name, err, result && result.css, doReplacements, result && result.map, baseFolder, result && result.imports, getFilename
|
||||
);
|
||||
release();
|
||||
return verificationResult;
|
||||
@@ -439,31 +738,90 @@ module.exports = function() {
|
||||
var css_name = name;
|
||||
if (nameModifier) { css_name = nameModifier(name); }
|
||||
|
||||
fs.readFile(path.join(testFolder, 'css', css_name) + '.css', 'utf8', function (e, css) {
|
||||
process.stdout.write('- ' + path.join(baseFolder, css_name) + ': ');
|
||||
// Check if we're using the new co-located structure (tests-unit/ or tests-config/) or the old separated structure
|
||||
var cssPath;
|
||||
if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) {
|
||||
// New co-located structure: CSS file is in the same directory as LESS file
|
||||
cssPath = path.join(path.dirname(fullPath), path.basename(file, '.less') + '.css');
|
||||
} else {
|
||||
// Old separated structure: CSS file is in separate css/ folder
|
||||
// Windows compatibility: css_name may already contain path separators
|
||||
// Use path.join with empty string to let path.join handle normalization
|
||||
cssPath = path.join(testFolder, css_name) + '.css';
|
||||
}
|
||||
|
||||
css = css && doReplacements(css, path.join(baseFolder, foldername));
|
||||
if (result.css === css) { ok('OK'); }
|
||||
else {
|
||||
difference('FAIL', css, result.css);
|
||||
// For the new structure, we need to handle replacements differently
|
||||
var replacementPath;
|
||||
if (relativePath.startsWith('tests-unit/') || relativePath.startsWith('tests-config/')) {
|
||||
replacementPath = path.dirname(fullPath);
|
||||
// Ensure replacementPath ends with a path separator for consistent matching
|
||||
if (!replacementPath.endsWith(path.sep)) {
|
||||
replacementPath += path.sep;
|
||||
}
|
||||
release();
|
||||
});
|
||||
} else {
|
||||
replacementPath = path.join(baseFolder, relativePath);
|
||||
}
|
||||
|
||||
var testName = fullPath.replace(/\.less$/, '');
|
||||
process.stdout.write('- ' + testName + ': ');
|
||||
|
||||
|
||||
var css = fs.readFileSync(cssPath, 'utf8');
|
||||
css = css && doReplacements(css, replacementPath);
|
||||
if (result.css === css) { ok('OK'); }
|
||||
else {
|
||||
difference('FAIL', css, result.css);
|
||||
}
|
||||
|
||||
release();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBasename(file, relativePath) {
|
||||
var basePath = relativePath || foldername;
|
||||
// Ensure basePath ends with a slash for proper path construction
|
||||
if (basePath.charAt(basePath.length - 1) !== '/') {
|
||||
basePath = basePath + '/';
|
||||
}
|
||||
return basePath + path.basename(file, '.less');
|
||||
}
|
||||
|
||||
|
||||
// This function is only called for non-glob patterns now
|
||||
// For glob patterns, we use the glob library in the calling code
|
||||
var dirPath = path.join(baseFolder, foldername);
|
||||
var items = fs.readdirSync(dirPath);
|
||||
|
||||
items.forEach(function(item) {
|
||||
if (/\.less$/.test(item)) {
|
||||
processFileWithInfo({
|
||||
file: item,
|
||||
fullPath: path.join(dirPath, item),
|
||||
relativePath: foldername
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function diff(left, right) {
|
||||
require('diff').diffLines(left, right).forEach(function(item) {
|
||||
if (item.added || item.removed) {
|
||||
var text = item.value && item.value.replace('\n', String.fromCharCode(182) + '\n').replace('\ufeff', '[[BOM]]');
|
||||
process.stdout.write(stylize(text, item.added ? 'green' : 'red'));
|
||||
} else {
|
||||
process.stdout.write(item.value && item.value.replace('\ufeff', '[[BOM]]'));
|
||||
}
|
||||
// Configure chalk to always show colors
|
||||
var chalk = require('chalk');
|
||||
chalk.level = 3; // Force colors on
|
||||
|
||||
// Use jest-diff for much clearer output like Vitest
|
||||
var diffResult = require('jest-diff').diffStringsUnified(left || '', right || '', {
|
||||
expand: false,
|
||||
includeChangeCounts: true,
|
||||
contextLines: 1,
|
||||
aColor: chalk.red,
|
||||
bColor: chalk.green,
|
||||
changeColor: chalk.inverse,
|
||||
commonColor: chalk.dim
|
||||
});
|
||||
process.stdout.write('\n');
|
||||
|
||||
// jest-diff returns a string with ANSI colors, so we can output it directly
|
||||
process.stdout.write(diffResult + '\n');
|
||||
}
|
||||
|
||||
function fail(msg) {
|
||||
@@ -476,6 +834,9 @@ module.exports = function() {
|
||||
process.stdout.write(stylize(msg, 'yellow') + '\n');
|
||||
failedTests++;
|
||||
|
||||
// Only show the diff, not the full text
|
||||
process.stdout.write(stylize('Diff:', 'yellow') + '\n');
|
||||
|
||||
diff(left || '', right || '');
|
||||
endTest();
|
||||
}
|
||||
@@ -528,27 +889,41 @@ module.exports = function() {
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function toCSS(options, filePath, callback) {
|
||||
options = options || {};
|
||||
// Deep clone options to prevent modifying the original, but preserve functions
|
||||
var originalOptions = options || {};
|
||||
options = JSON.parse(JSON.stringify(originalOptions));
|
||||
|
||||
// Restore functions that were lost in JSON serialization
|
||||
if (originalOptions.getVars) {
|
||||
options.getVars = originalOptions.getVars;
|
||||
}
|
||||
var str = fs.readFileSync(filePath, 'utf8'), addPath = path.dirname(filePath);
|
||||
|
||||
// Initialize paths array if it doesn't exist
|
||||
if (typeof options.paths !== 'string') {
|
||||
options.paths = options.paths || [];
|
||||
if (!contains(options.paths, addPath)) {
|
||||
options.paths.push(addPath);
|
||||
}
|
||||
} else {
|
||||
options.paths = [options.paths]
|
||||
options.paths = [options.paths];
|
||||
}
|
||||
|
||||
// Add the current directory to paths if not already present
|
||||
if (!contains(options.paths, addPath)) {
|
||||
options.paths.push(addPath);
|
||||
}
|
||||
|
||||
// Resolve all paths relative to the test file's directory
|
||||
options.paths = options.paths.map(searchPath => {
|
||||
return path.resolve(lessFolder, searchPath)
|
||||
if (path.isAbsolute(searchPath)) {
|
||||
return searchPath;
|
||||
}
|
||||
// Resolve relative to the test file's directory
|
||||
return path.resolve(path.dirname(filePath), searchPath);
|
||||
})
|
||||
|
||||
options.filename = path.resolve(process.cwd(), filePath);
|
||||
options.optimization = options.optimization || 0;
|
||||
|
||||
if (options.globalVars) {
|
||||
options.globalVars = options.getVars(filePath);
|
||||
} else if (options.modifyVars) {
|
||||
options.modifyVars = options.getVars(filePath);
|
||||
}
|
||||
// Note: globalVars and modifyVars are now handled via styles.config.cjs or lessOptions
|
||||
if (options.plugin) {
|
||||
var Plugin = require(path.resolve(process.cwd(), options.plugin));
|
||||
options.plugins = [Plugin];
|
||||
@@ -571,22 +946,7 @@ module.exports = function() {
|
||||
ok(stylize('OK\n', 'green'));
|
||||
}
|
||||
|
||||
function testImportRedirect(nockScope) {
|
||||
return (name, err, css, doReplacements, sourcemap, baseFolder) => {
|
||||
process.stdout.write('- ' + path.join(baseFolder, name) + ': ');
|
||||
if (err) {
|
||||
fail('FAIL: ' + (err && err.message));
|
||||
return;
|
||||
}
|
||||
const expected = 'h1 {\n color: red;\n}\n';
|
||||
if (css !== expected) {
|
||||
difference('FAIL', expected, css);
|
||||
return;
|
||||
}
|
||||
nockScope.done();
|
||||
ok('OK');
|
||||
};
|
||||
}
|
||||
// HTTP redirect testing is now handled directly in test/index.js
|
||||
|
||||
function testDisablePluginRule() {
|
||||
less.render(
|
||||
@@ -615,7 +975,6 @@ module.exports = function() {
|
||||
testSourcemapWithoutUrlAnnotation: testSourcemapWithoutUrlAnnotation,
|
||||
testSourcemapWithVariableInSelector: testSourcemapWithVariableInSelector,
|
||||
testImports: testImports,
|
||||
testImportRedirect: testImportRedirect,
|
||||
testEmptySourcemap: testEmptySourcemap,
|
||||
testNoOptions: testNoOptions,
|
||||
testDisablePluginRule: testDisablePluginRule,
|
||||
|
||||
1
packages/less/test/sourcemaps/comprehensive.json
Normal file
1
packages/less/test/sourcemaps/comprehensive.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["comprehensive.less"],"names":[],"mappings":"AAoBA;EACE,aAAA;EACA,mBAAA;;AAFF,UAIE;EACE,YAAA;EACA,eAAA;;AANJ,UAIE,QAIE;EACE,iBAAA;EACA,mBAAA;;AAVN,UAcE;EACE,mBAAA;EACA,aAAA;;AAhBJ,UAcE,SAIE;EACE,SAAA;EACA,gBAAA;;AAMN;EACE,OAAO,qBAAP;EACA,QAAQ,iBAAR;EACA,YAAA;;AAIF;EACE,cAAA;EACA,mBAAA;EACA,yCAAA;;AAIF;EAlDE,mBAAA;EACA,2BAAA;EACA,wBAAA;EAIA,yCAAA;EA+CA,aAAA;EACA,iBAAA;;AAIF,QAA0B;EACxB;IACE,aAAA;;EADF,UAGE;IACE,eAAA;;;AAKN;EACE;IACE,aAAA;IACA,uBAAuB,cAAvB;IACA,SAAA;;;AAKJ;AAMA;EALE,kBAAA;EACA,YAAA;EACA,eAAA;;AAGF;EAEE,mBAAA;EACA,YAAA;;AAIF,WACE;EACE,gBAAA;;AAFJ,WACE,GAGE;EACE,qBAAA;;AALN,WACE,GAGE,GAGE;EACE,qBAAA;;AAEA,WATN,GAGE,GAGE,EAGG;EACC,cAAA;;AAGF,WAbN,GAGE,GAGE,EAOG;EACC,cAAA;;AAYT;EACC,cAAA;;AAIF,OACE,QACE,QACE;EACE,cAAA","file":"{path}comprehensive.css"}
|
||||
1
packages/less/test/sourcemaps/sourcemaps-basepath.json
Normal file
1
packages/less/test/sourcemaps/sourcemaps-basepath.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["testweb/sourcemaps-basepath.less"],"names":[],"mappings":"AAEA;EACE,eAAA;EACA,gBAAA;;AAGF;EACE,iBAAA","file":"tests-config/sourcemaps-basepath/sourcemaps-basepath.css"}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["testweb/sourcemaps-include-source.less"],"names":[],"mappings":"AAGA;EACE,mBAAA;EACA,YAAA;EACA,kBAAA;;AAGF;EACE,mBAAA","file":"tests-config/sourcemaps-include-source/sourcemaps-include-source.css","sourcesContent":["@primary: #007bff;\n@secondary: #6c757d;\n\n.button {\n background: @primary;\n color: white;\n padding: 10px 20px;\n}\n\n.secondary {\n background: @secondary;\n}\n\n"]}
|
||||
1
packages/less/test/sourcemaps/sourcemaps-rootpath.json
Normal file
1
packages/less/test/sourcemaps/sourcemaps-rootpath.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["https://example.com/less/sourcemaps-rootpath.less"],"names":[],"mappings":"AAEA;EACE,aAAA;EACA,YAAA;;AAGF;EACE,WAAA","file":"tests-config/sourcemaps-rootpath/sourcemaps-rootpath.css"}
|
||||
1
packages/less/test/sourcemaps/sourcemaps-url.json
Normal file
1
packages/less/test/sourcemaps/sourcemaps-url.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["testweb/sourcemaps-url.less"],"names":[],"mappings":"AAEA;EACE,UAAA;EACA,gBAAA;;AAGF;EACE,YAAA","file":"tests-config/sourcemaps-url/sourcemaps-url.css"}
|
||||
@@ -1,140 +0,0 @@
|
||||
#yelow #short {
|
||||
color: #fea;
|
||||
}
|
||||
#yelow #long {
|
||||
color: #ffeeaa;
|
||||
}
|
||||
#yelow #rgba {
|
||||
color: rgba(255, 238, 170, 0.1);
|
||||
}
|
||||
#yelow #argb {
|
||||
color: #1affeeaa;
|
||||
}
|
||||
#blue #short {
|
||||
color: #00f;
|
||||
}
|
||||
#blue #long {
|
||||
color: #0000ff;
|
||||
}
|
||||
#blue #rgba {
|
||||
color: rgba(0, 0, 255, 0.1);
|
||||
}
|
||||
#blue #argb {
|
||||
color: #1a0000ff;
|
||||
}
|
||||
#alpha #hsla {
|
||||
color: hsla(11, 20%, 20%, 0.6);
|
||||
}
|
||||
#overflow .a {
|
||||
color: #000000;
|
||||
}
|
||||
#overflow .b {
|
||||
color: #ffffff;
|
||||
}
|
||||
#overflow .c {
|
||||
color: #ffffff;
|
||||
}
|
||||
#overflow .d {
|
||||
color: #00ff00;
|
||||
}
|
||||
#overflow .e {
|
||||
color: rgba(0, 31, 255, 0.42);
|
||||
}
|
||||
#grey {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
#aa3333 {
|
||||
color: #aa3333;
|
||||
}
|
||||
#bb8080 {
|
||||
color: hsl(0, 30%, 62%);
|
||||
}
|
||||
#ccff00 {
|
||||
color: hsl(72, 100%, 50%);
|
||||
}
|
||||
.lightenblue {
|
||||
color: #3333ff;
|
||||
}
|
||||
.darkenblue {
|
||||
color: #0000cc;
|
||||
}
|
||||
.unknowncolors {
|
||||
color: blue2;
|
||||
border: 2px solid superred;
|
||||
}
|
||||
.transparent {
|
||||
color: transparent;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
#alpha #fromvar {
|
||||
opacity: 0.7;
|
||||
}
|
||||
#alpha #short {
|
||||
opacity: 1;
|
||||
}
|
||||
#alpha #long {
|
||||
opacity: 1;
|
||||
}
|
||||
#alpha #rgba {
|
||||
opacity: 0.2;
|
||||
}
|
||||
#alpha #hsl {
|
||||
opacity: 1;
|
||||
}
|
||||
#percentage {
|
||||
color: 255;
|
||||
border-color: rgba(255, 0, 0, 0.5);
|
||||
}
|
||||
#rrggbbaa {
|
||||
test-1: #55FF5599;
|
||||
test-2: #5F59;
|
||||
test-3: rgba(136, 255, 136, 0.6);
|
||||
test-4: rgba(85, 255, 85, 0.1);
|
||||
test-5: rgba(85, 255, 85, 0.6);
|
||||
test-6: rgba(85, 255, 85, 0.6);
|
||||
test-7: rgba(85, 255, 85, 0.5);
|
||||
test-8: rgba(var(--color-accent), 0.2);
|
||||
test-9: rgb(var(--color-accent));
|
||||
test-9: hsla(var(--color-accent));
|
||||
test-10: #55FF5599;
|
||||
test-11: hsla(120, 100%, 66.66666667%, 0.6);
|
||||
test-12: hsla(120, 100%, 66.66666667%, 0.5);
|
||||
--semi-transparent-dark-background: #001e00ee;
|
||||
--semi-transparent-dark-background-2: #001e00;
|
||||
}
|
||||
.color-oklch-sub {
|
||||
background: oklch(from #0000FF calc(l - 0.1) c h);
|
||||
}
|
||||
.color-oklch-add {
|
||||
background: oklch(from #0000FF calc(l + 0.1) c h);
|
||||
}
|
||||
.color-oklch-mult {
|
||||
background: oklch(from #0000FF calc(l * 0.1) c h);
|
||||
}
|
||||
.color-oklch-div {
|
||||
background: oklch(from #0000FF calc(l / 2) c h);
|
||||
}
|
||||
.color-hsl-sub {
|
||||
background: hsl(from #0000FF calc(h - 1) s l);
|
||||
}
|
||||
.color-hsl-add {
|
||||
background: hsl(from #0000FF calc(h + 1) s l);
|
||||
}
|
||||
.color-hsl-mult {
|
||||
background: hsl(from #0000FF calc(h * 1) s l);
|
||||
}
|
||||
.color-hsl-div {
|
||||
background: hsl(from #0000FF calc(h / 2) s l);
|
||||
}
|
||||
.color-rgb-sub {
|
||||
background: rgb(from #0000FF calc(r - 1) g b);
|
||||
}
|
||||
.color-rgb-add {
|
||||
background: rgb(from #0000FF calc(r + 1) g b);
|
||||
}
|
||||
.color-rgb-mult {
|
||||
background: rgb(from #0000FF calc(r * 1) g b);
|
||||
}
|
||||
.color-rgb-div {
|
||||
background: rgb(from #0000FF calc(r / 2) g b);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
.variables {
|
||||
width: 14cm;
|
||||
}
|
||||
.variable-dash .q {
|
||||
padding: 30px 15px;
|
||||
}
|
||||
.variables {
|
||||
height: 24px;
|
||||
color: #888;
|
||||
font-family: "Trebuchet MS", Verdana, sans-serif;
|
||||
quotes: "~" "~";
|
||||
}
|
||||
.redef {
|
||||
zero: 0;
|
||||
}
|
||||
.redef .inition {
|
||||
three: 3;
|
||||
}
|
||||
.values {
|
||||
minus-one: -1;
|
||||
font-family: 'Trebuchet', 'Trebuchet', 'Trebuchet';
|
||||
color: #888 !important;
|
||||
same-color: #888 !important;
|
||||
same-again: #888 !important;
|
||||
multi-important: #888 #888, 'Trebuchet' !important;
|
||||
multi: something 'A', B, C, 'Trebuchet';
|
||||
}
|
||||
.variable-names .quoted {
|
||||
name: 'hello';
|
||||
}
|
||||
.variable-names .unquoted {
|
||||
name: 'hello';
|
||||
}
|
||||
.variable-names .color-keyword {
|
||||
name: 'hello';
|
||||
}
|
||||
.alpha {
|
||||
filter: alpha(opacity=42);
|
||||
}
|
||||
.test-rulePollution {
|
||||
a: 'no-pollution';
|
||||
}
|
||||
.units {
|
||||
width: 1px;
|
||||
same-unit-as-previously: 1px;
|
||||
square-pixel-divided: 1px;
|
||||
odd-unit: 2;
|
||||
percentage: 500%;
|
||||
pixels: 500px;
|
||||
conversion-metric-a: 30mm;
|
||||
conversion-metric-b: 3cm;
|
||||
conversion-imperial: 3in;
|
||||
custom-unit: 420octocats;
|
||||
custom-unit-cancelling: 18dogs;
|
||||
mix-units: 2px;
|
||||
invalid-units: 1px;
|
||||
}
|
||||
.units .fallback {
|
||||
div-px-1: 10px;
|
||||
div-px-2: 1px;
|
||||
sub-px-1: 12.6px;
|
||||
sub-cm-1: 9.666625cm;
|
||||
mul-px-1: 19.6px;
|
||||
mul-em-1: 19.6em;
|
||||
mul-em-2: 196em;
|
||||
mul-cm-1: 196cm;
|
||||
add-px-1: 15.4px;
|
||||
add-px-2: 393.35275591px;
|
||||
mul-px-2: 140px;
|
||||
mul-px-3: 140px;
|
||||
}
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
}
|
||||
.radio_checked {
|
||||
border-color: #fff;
|
||||
}
|
||||
div#apple {
|
||||
color: blue;
|
||||
}
|
||||
div#banana {
|
||||
color: blue;
|
||||
}
|
||||
div#cherry {
|
||||
color: blue;
|
||||
}
|
||||
div#carrot {
|
||||
color: blue;
|
||||
}
|
||||
div#potato {
|
||||
color: blue;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 173 B |
@@ -1,3 +0,0 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
@import "import/import-charset-test";
|
||||
@@ -1,164 +0,0 @@
|
||||
#yelow {
|
||||
#short {
|
||||
color: #fea;
|
||||
}
|
||||
#long {
|
||||
color: #ffeeaa;
|
||||
}
|
||||
#rgba {
|
||||
color: rgba(255, 238, 170, 0.1);
|
||||
}
|
||||
#argb {
|
||||
color: argb(rgba(255, 238, 170, 0.1));
|
||||
}
|
||||
}
|
||||
|
||||
#blue {
|
||||
#short {
|
||||
color: #00f;
|
||||
}
|
||||
#long {
|
||||
color: #0000ff;
|
||||
}
|
||||
#rgba {
|
||||
color: rgba(0, 0, 255, 0.1);
|
||||
}
|
||||
#argb {
|
||||
color: argb(rgba(0, 0, 255, 0.1));
|
||||
}
|
||||
}
|
||||
|
||||
#alpha #hsla {
|
||||
color: hsla(11, 20%, 20%, 0.6);
|
||||
}
|
||||
|
||||
#overflow {
|
||||
.a { color: (#111111 - #444444); } // #000000
|
||||
.b { color: (#eee + #fff); } // #ffffff
|
||||
.c { color: (#aaa * 3); } // #ffffff
|
||||
.d { color: (#00ee00 + #009900); } // #00ff00
|
||||
.e { color: rgba(-99.9, 31.4159, 321, 0.42); }
|
||||
}
|
||||
|
||||
#grey {
|
||||
color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
#aa3333 {
|
||||
color: rgb(66.66%, 20%, 20%);
|
||||
}
|
||||
|
||||
#bb8080 {
|
||||
color: hsl(0deg, 30%, 62%);
|
||||
}
|
||||
|
||||
#ccff00 {
|
||||
color: hsl(72deg, 100%, 50%);
|
||||
}
|
||||
|
||||
.lightenblue {
|
||||
color: lighten(blue, 10%);
|
||||
}
|
||||
|
||||
.darkenblue {
|
||||
color: darken(blue, 10%);
|
||||
}
|
||||
|
||||
.unknowncolors {
|
||||
color: blue2;
|
||||
border: 2px solid superred;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
color: transparent;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
#alpha {
|
||||
@colorvar: rgba(150, 200, 150, 0.7);
|
||||
#fromvar {
|
||||
opacity: alpha(@colorvar);
|
||||
}
|
||||
#short {
|
||||
opacity: alpha(#aaa);
|
||||
}
|
||||
#long {
|
||||
opacity: alpha(#bababa);
|
||||
}
|
||||
#rgba {
|
||||
opacity: alpha(rgba(50, 120, 95, 0.2));
|
||||
}
|
||||
#hsl {
|
||||
opacity: alpha(hsl(120, 100%, 50%));
|
||||
}
|
||||
}
|
||||
|
||||
#percentage {
|
||||
color: red(rgb(100%, 0, 0));
|
||||
border-color: rgba(100%, 0, 0, 50%);
|
||||
}
|
||||
|
||||
#rrggbbaa {
|
||||
test-1: #55FF5599;
|
||||
test-2: #5F59;
|
||||
test-3: lighten(#55FF5599, 10%);
|
||||
test-4: fade(#5F59, 10%);
|
||||
test-5: rgba(#55FF5599);
|
||||
test-6: rgba(#5F59);
|
||||
test-7: rgba(#5F59, 0.5);
|
||||
test-8: rgba(var(--color-accent), 0.2);
|
||||
test-9: rgb(var(--color-accent));
|
||||
test-9: hsla(var(--color-accent));
|
||||
test-10: color('#55FF5599');
|
||||
test-11: hsla(#5F59);
|
||||
test-12: hsla(#5F59, 0.5);
|
||||
--semi-transparent-dark-background: #001e00ee;
|
||||
--semi-transparent-dark-background-2: rgba(0, 30, 0, 238); // invalid opacity will be capped
|
||||
}
|
||||
|
||||
.color-oklch-sub {
|
||||
background: oklch(from #0000FF calc(l - 0.1) c h);
|
||||
}
|
||||
|
||||
.color-oklch-add {
|
||||
background: oklch(from #0000FF calc(l + 0.1) c h);
|
||||
}
|
||||
|
||||
.color-oklch-mult {
|
||||
background: oklch(from #0000FF calc(l * 0.1) c h);
|
||||
}
|
||||
|
||||
.color-oklch-div {
|
||||
background: oklch(from #0000FF calc(l / 2) c h);
|
||||
}
|
||||
|
||||
.color-hsl-sub {
|
||||
background: hsl(from #0000FF calc(h - 1) s l);
|
||||
}
|
||||
|
||||
.color-hsl-add {
|
||||
background: hsl(from #0000FF calc(h + 1) s l);
|
||||
}
|
||||
|
||||
.color-hsl-mult {
|
||||
background: hsl(from #0000FF calc(h * 1) s l);
|
||||
}
|
||||
|
||||
.color-hsl-div {
|
||||
background: hsl(from #0000FF calc(h / 2) s l);
|
||||
}
|
||||
|
||||
.color-rgb-sub {
|
||||
background: rgb(from #0000FF calc(r - 1) g b);
|
||||
}
|
||||
|
||||
.color-rgb-add {
|
||||
background: rgb(from #0000FF calc(r + 1) g b);
|
||||
}
|
||||
|
||||
.color-rgb-mult {
|
||||
background: rgb(from #0000FF calc(r * 1) g b);
|
||||
}
|
||||
|
||||
.color-rgb-div {
|
||||
background: rgb(from #0000FF calc(r / 2) g b);
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
@a: 2;
|
||||
@x: (@a * @a);
|
||||
@y: (@x + 1);
|
||||
@z: (@x * 2 + @y);
|
||||
@var: -1;
|
||||
|
||||
.variables {
|
||||
width: (@z + 1cm); // 14cm
|
||||
}
|
||||
|
||||
.variable-dash {
|
||||
@jumbotron-padding: 30px;
|
||||
|
||||
.q {
|
||||
padding: @jumbotron-padding (@jumbotron-padding/2);
|
||||
}
|
||||
}
|
||||
|
||||
@b: @a * 10;
|
||||
@c: #888;
|
||||
|
||||
@fonts: "Trebuchet MS", Verdana, sans-serif;
|
||||
@f: @fonts;
|
||||
|
||||
@quotes: "~" "~";
|
||||
@q: @quotes;
|
||||
@onePixel: 1px;
|
||||
|
||||
.variables {
|
||||
height: (@b + @x + 0px); // 24px
|
||||
color: @c;
|
||||
font-family: @f;
|
||||
quotes: @q;
|
||||
}
|
||||
|
||||
.redef {
|
||||
@var: 0;
|
||||
.inition {
|
||||
@var: 4;
|
||||
@var: 2;
|
||||
three: @var;
|
||||
@var: 3;
|
||||
}
|
||||
zero: @var;
|
||||
}
|
||||
|
||||
@important-var: @c !important;
|
||||
@important-var-two: @a !important;
|
||||
.values {
|
||||
minus-one: @var;
|
||||
@a: 'Trebuchet';
|
||||
@multi: 'A', B, C;
|
||||
font-family: @a, @a, @a;
|
||||
color: @c !important;
|
||||
same-color: @important-var;
|
||||
same-again: @important-var !important;
|
||||
multi-important: @important-var @important-var, @important-var-two;
|
||||
multi: something @multi, @a;
|
||||
}
|
||||
|
||||
.variable-names {
|
||||
.quoted {
|
||||
@var: 'hello';
|
||||
@name: 'var';
|
||||
name: @@name;
|
||||
}
|
||||
|
||||
.unquoted {
|
||||
@var: 'hello';
|
||||
@name: var;
|
||||
name: @@name;
|
||||
}
|
||||
|
||||
.color-keyword {
|
||||
@red: 'hello';
|
||||
@name: red;
|
||||
name: @@name;
|
||||
}
|
||||
}
|
||||
|
||||
.alpha {
|
||||
@var: 42;
|
||||
filter: alpha(opacity=@var);
|
||||
}
|
||||
|
||||
.polluteMixin() {
|
||||
@a: 'pollution';
|
||||
}
|
||||
.test-rulePollution {
|
||||
@a: 'no-pollution';
|
||||
a: @a;
|
||||
.polluteMixin();
|
||||
a: @a;
|
||||
}
|
||||
|
||||
.units {
|
||||
width: @onePixel;
|
||||
same-unit-as-previously: (@onePixel / @onePixel);
|
||||
square-pixel-divided: (@onePixel * @onePixel / @onePixel);
|
||||
odd-unit: unit((@onePixel * 4em / 2cm));
|
||||
percentage: (10 * 50%);
|
||||
pixels: (50px * 10);
|
||||
conversion-metric-a: (20mm + 1cm);
|
||||
conversion-metric-b: (1cm + 20mm);
|
||||
conversion-imperial: (1in + 72pt + 6pc);
|
||||
custom-unit: (42octocats * 10);
|
||||
custom-unit-cancelling: (8cats * 9dogs / 4cats);
|
||||
mix-units: (1px + 1em);
|
||||
invalid-units: (1px * 1px);
|
||||
.fallback {
|
||||
@px: 14px;
|
||||
@em: 1.4em;
|
||||
@cm: 10cm;
|
||||
div-px-1: (@px / @em);
|
||||
div-px-2: ((@px / @em) / @cm);
|
||||
sub-px-1: (@px - @em);
|
||||
sub-cm-1: (@cm - (@px - @em));
|
||||
mul-px-1: (@px * @em);
|
||||
mul-em-1: (@em * @px);
|
||||
mul-em-2: ((@em * @px) * @cm);
|
||||
mul-cm-1: (@cm * (@em * @px));
|
||||
add-px-1: (@px + @em);
|
||||
add-px-2: ((@px + @em) + @cm);
|
||||
mul-px-2: ((1 * @px) * @cm);
|
||||
mul-px-3: ((@px * 1) * @cm);
|
||||
}
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
}
|
||||
|
||||
@a1: 1px;
|
||||
@b2: 2px;
|
||||
@c3: @a1 + @b2;
|
||||
|
||||
@radio-cls: radio;
|
||||
@radio-cls-checked: @{radio-cls}_checked;
|
||||
|
||||
.@{radio-cls-checked} {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
@items:
|
||||
// Fruit
|
||||
apple,
|
||||
banana,
|
||||
cherry,
|
||||
// Vegetables
|
||||
carrot,
|
||||
potato,
|
||||
;
|
||||
|
||||
each(@items, {
|
||||
div#@{value} {
|
||||
color: blue;
|
||||
}
|
||||
})
|
||||
7
packages/test-data/tests-config/3rd-party/styles.config.cjs
vendored
Normal file
7
packages/test-data/tests-config/3rd-party/styles.config.cjs
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"math": 0
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
@charset "UTF-8";@media screen,print,handheld{body{font-size:12pt}}@media screen{.container{color:red;background:blue}}@media screen{.test{color:red;background:blue}}@media screen,print{body{margin:0}}@media screen{.container{color:red}.container .child{color:blue}}@media screen{.wrapper .inner{color:green}}@page{margin:2cm;size:A4}@supports (display: grid){.grid{display:grid}.flex{display:flex}}@media screen{body{color:black}}@layer base{body{margin:0}p{padding:0}}@media screen{.test{color:value}}@media screen and print{body{color:black}}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Test at-rule evaluation paths (eval, evalRoot, genCSS, accept, etc.)
|
||||
|
||||
// Test keywordList - @media with keyword list
|
||||
@media screen, print, handheld {
|
||||
body {
|
||||
font-size: 12pt;
|
||||
}
|
||||
}
|
||||
|
||||
// Test declarationsBlock with mergeable=true (nested ruleset with declarations)
|
||||
@media screen {
|
||||
.container {
|
||||
color: red;
|
||||
background: blue;
|
||||
}
|
||||
}
|
||||
|
||||
// Test simpleBlock optimization with allRulesetDeclarations (single ruleset)
|
||||
@media screen {
|
||||
.test {
|
||||
color: red;
|
||||
background: blue;
|
||||
}
|
||||
}
|
||||
|
||||
// Test eval with value evaluation and keywordList conversion
|
||||
@breakpoint: screen;
|
||||
@media @breakpoint, print {
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Test evalRoot with ampersand handling
|
||||
.container {
|
||||
@media screen {
|
||||
& {
|
||||
color: red;
|
||||
}
|
||||
.child {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test evalRoot with mixed ampersands
|
||||
.wrapper {
|
||||
.inner {
|
||||
@media screen {
|
||||
& {
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test genCSS with value
|
||||
@charset "UTF-8";
|
||||
|
||||
// Test genCSS with simpleBlock
|
||||
@page {
|
||||
margin: 2cm;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
// Test genCSS with rules (non-simpleBlock)
|
||||
@supports (display: grid) {
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
// Test isCharset
|
||||
@charset "UTF-8";
|
||||
|
||||
// Test isRulesetLike (non-charset)
|
||||
@media screen {
|
||||
body {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
// Test compressed output (outputRuleset compressed path)
|
||||
@layer base {
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Test variable/find/rulesets delegation
|
||||
@media screen {
|
||||
@var: value;
|
||||
.test {
|
||||
color: @var;
|
||||
}
|
||||
}
|
||||
|
||||
// Test eval with media bubbling
|
||||
@media screen {
|
||||
@media print {
|
||||
body {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
compress: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
@media screen{body{margin:0}p{padding:0}}@layer base{body{margin:0}p{padding:0}div{border:0}}@supports (display: grid){.grid{display:grid}.item{grid-column:1}}@page{margin:2cm;size:A4}@keyframes slide{from{transform:translateX(0)}to{transform:translateX(100px)}}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Tests for atrule.js coverage - compressed output path (lines 243-250)
|
||||
// Also covers parser + genCSS + outputRuleset
|
||||
|
||||
// Compressed @media with rules
|
||||
@media screen {
|
||||
body { margin: 0; }
|
||||
p { padding: 0; }
|
||||
}
|
||||
|
||||
// Compressed @layer with multiple rules
|
||||
@layer base {
|
||||
body { margin: 0; }
|
||||
p { padding: 0; }
|
||||
div { border: 0; }
|
||||
}
|
||||
|
||||
// Compressed @supports
|
||||
@supports (display: grid) {
|
||||
.grid { display: grid; }
|
||||
.item { grid-column: 1; }
|
||||
}
|
||||
|
||||
// Compressed @page
|
||||
@page {
|
||||
margin: 2cm;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
// Compressed @keyframes
|
||||
@keyframes slide {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(100px); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
compress: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"math": "strict",
|
||||
"compress": true
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
// Entry file for -all configuration
|
||||
@import "../linenumbers.less";
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"math": "strict",
|
||||
"dumpLineNumbers": "all"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
// Entry file for -comments configuration
|
||||
@import "../linenumbers.less";
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"math": "strict",
|
||||
"dumpLineNumbers": "comments"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
// Entry file for -mediaquery configuration
|
||||
@import "../linenumbers.less";
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"math": "strict",
|
||||
"dumpLineNumbers": "mediaquery"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"plugin": "test/plugins/filemanager/"
|
||||
}
|
||||
}
|
||||
};
|
||||
13
packages/test-data/tests-config/globalVars/styles.config.cjs
Normal file
13
packages/test-data/tests-config/globalVars/styles.config.cjs
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
globalVars: {
|
||||
'my-color': 'red',
|
||||
'base-color': '#111',
|
||||
'the-border': '1px',
|
||||
'red': '#842210'
|
||||
},
|
||||
banner: '/**\n * Test\n */\n'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"paths": ["../../data/"]
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"paths": [
|
||||
"../../data/"
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
math: 'strict',
|
||||
strictUnits: true,
|
||||
javascriptEnabled: true
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"math": "always"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"math": "parens-division"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
language: {
|
||||
less: {
|
||||
"math": "parens"
|
||||
}
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user