diff --git a/test/fixtures/folder (with) special [chars] & more/test+file#1.html b/test/fixtures/folder (with) special [chars] & more/test+file#1.html new file mode 100644 index 0000000..edef6cd --- /dev/null +++ b/test/fixtures/folder (with) special [chars] & more/test+file#1.html @@ -0,0 +1,10 @@ + + + + Test File with Special Characters + + + +
+ + diff --git a/test/fixtures/folder with spaces/test file.html b/test/fixtures/folder with spaces/test file.html new file mode 100644 index 0000000..644e1cd --- /dev/null +++ b/test/fixtures/folder with spaces/test file.html @@ -0,0 +1,10 @@ + + + + Test File with Spaces + + + +
+ + diff --git a/test/html_test.js b/test/html_test.js index 80dd5dc..3870067 100644 --- a/test/html_test.js +++ b/test/html_test.js @@ -179,4 +179,140 @@ describe('htmllint', () => { run(options, expected, '3 errors from 3 files', done); }); }); + + describe('file paths with special characters', () => { + it('with spaces (relative)', done => { + const filePath = path.normalize('test/fixtures/folder with spaces/test file.html'); + const options = { + files: [filePath], + errorlevels: ['info', 'warning', 'error'] + }; + const expected = [ + { + file: filePath, + type: 'error', + message: expectedResults.invalid[1].message, + lastLine: 7, + lastColumn: 39 + }, + { + file: filePath, + type: 'error', + message: expectedResults.invalid[2].message, + lastLine: 7, + lastColumn: 39 + }, + { + file: filePath, + type: 'error', + message: expectedResults.invalid[3].message, + lastLine: 8, + lastColumn: 20 + } + ]; + + run(options, expected, 'errors from file path with spaces', done); + }); + + it('with spaces (absolute)', done => { + const filePath = path.resolve('test/fixtures/folder with spaces/test file.html'); + const options = { + files: [filePath], + absoluteFilePathsForReporter: true, + errorlevels: ['info', 'warning', 'error'] + }; + const expected = [ + { + file: filePath, + type: 'error', + message: expectedResults.invalid[1].message, + lastLine: 7, + lastColumn: 39 + }, + { + file: filePath, + type: 'error', + message: expectedResults.invalid[2].message, + lastLine: 7, + lastColumn: 39 + }, + { + file: filePath, + type: 'error', + message: expectedResults.invalid[3].message, + lastLine: 8, + lastColumn: 20 + } + ]; + + run(options, expected, 'errors from absolute file path with spaces', done); + }); + + it('with various special characters (relative)', done => { + const filePath = path.normalize('test/fixtures/folder (with) special [chars] & more/test+file#1.html'); + const options = { + files: [filePath], + errorlevels: ['info', 'warning', 'error'] + }; + const expected = [ + { + file: filePath, + type: 'error', + message: expectedResults.invalid[1].message.replace('unknownattr', 'invalidattr'), + lastLine: 7, + lastColumn: 39 + }, + { + file: filePath, + type: 'error', + message: expectedResults.invalid[2].message, + lastLine: 7, + lastColumn: 39 + }, + { + file: filePath, + type: 'error', + message: expectedResults.invalid[3].message, + lastLine: 8, + lastColumn: 21 + } + ]; + + run(options, expected, 'errors from file path with special characters', done); + }); + + it('with various special characters (absolute)', done => { + const filePath = path.resolve('test/fixtures/folder (with) special [chars] & more/test+file#1.html'); + const options = { + files: [filePath], + absoluteFilePathsForReporter: true, + errorlevels: ['info', 'warning', 'error'] + }; + const expected = [ + { + file: filePath, + type: 'error', + message: expectedResults.invalid[1].message.replace('unknownattr', 'invalidattr'), + lastLine: 7, + lastColumn: 39 + }, + { + file: filePath, + type: 'error', + message: expectedResults.invalid[2].message, + lastLine: 7, + lastColumn: 39 + }, + { + file: filePath, + type: 'error', + message: expectedResults.invalid[3].message, + lastLine: 8, + lastColumn: 21 + } + ]; + + run(options, expected, 'errors from absolute file path with special characters', done); + }); + }); }); diff --git a/test/processErrorMessages_test.js b/test/processErrorMessages_test.js new file mode 100644 index 0000000..608ff71 --- /dev/null +++ b/test/processErrorMessages_test.js @@ -0,0 +1,339 @@ +'use strict'; + +const assert = require('node:assert').strict; +const path = require('node:path'); +const processErrorMessages = require('../lib/processErrorMessages.js'); + +describe('processErrorMessages', () => { + describe('file path handling', () => { + it('should decode URL-encoded file paths with spaces', () => { + const errors = [{ + url: 'file:/C:/Users/test/Desktop/my%20project/test.html', + message: 'Test error', + type: 'error', + lastLine: 1, + lastColumn: 1 + }]; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + // The path should be decoded and relative + assert.ok(!result[0].file.includes('%20'), 'File path should not contain URL encoding'); + assert.ok(result[0].file.includes('my project'), 'File path should have decoded spaces'); + }); + + it('should decode URL-encoded file paths with special characters', () => { + const errors = [{ + url: 'file:/C:/Users/test/project%20with%20%26%20special/test.html', + message: 'Test error', + type: 'error', + lastLine: 1, + lastColumn: 1 + }]; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.ok(!result[0].file.includes('%20'), 'File path should not contain %20'); + assert.ok(!result[0].file.includes('%26'), 'File path should not contain %26'); + assert.ok(result[0].file.includes('&'), 'File path should have decoded ampersand'); + }); + + it('should decode parentheses in file paths', () => { + const errors = [{ + url: 'file:/C:/project%20%28test%29/file.html', + message: 'Test error', + type: 'error', + lastLine: 1, + lastColumn: 1 + }]; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.ok(!result[0].file.includes('%28'), 'File path should not contain %28'); + assert.ok(!result[0].file.includes('%29'), 'File path should not contain %29'); + assert.ok(result[0].file.includes('(test)'), 'File path should have decoded parentheses'); + }); + + it('should decode brackets and other special characters', () => { + const errors = [{ + url: 'file:/C:/folder%20%5Btest%5D%20%26%20more/file%2B1%23.html', + message: 'Test error', + type: 'error', + lastLine: 1, + lastColumn: 1 + }]; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.ok(!result[0].file.includes('%5B'), 'File path should not contain %5B'); + assert.ok(!result[0].file.includes('%5D'), 'File path should not contain %5D'); + assert.ok(!result[0].file.includes('%26'), 'File path should not contain %26'); + assert.ok(!result[0].file.includes('%2B'), 'File path should not contain %2B'); + assert.ok(!result[0].file.includes('%23'), 'File path should not contain %23'); + assert.ok(result[0].file.includes('[test]'), 'File path should have decoded brackets'); + assert.ok(result[0].file.includes('&'), 'File path should have decoded ampersand'); + assert.ok(result[0].file.includes('+'), 'File path should have decoded plus'); + assert.ok(result[0].file.includes('#'), 'File path should have decoded hash'); + }); + + it('should decode percent signs in file paths', () => { + const errors = [{ + url: 'file:/C:/project%2050%25/file.html', + message: 'Test error', + type: 'error', + lastLine: 1, + lastColumn: 1 + }]; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.ok(result[0].file.includes('50%'), 'File path should have decoded percent sign'); + }); + + it('should handle Unix-style file URLs', () => { + const errors = [{ + url: 'file:/home/user/my%20project/test.html', + message: 'Test error', + type: 'error', + lastLine: 1, + lastColumn: 1 + }]; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.ok(!result[0].file.includes('%20'), 'File path should not contain URL encoding'); + }); + + it('should convert to relative paths by default', () => { + const absolutePath = path.resolve('test/fixtures/invalid.html'); + const fileUrl = path.sep === '\\' ? + `file:/${absolutePath.replaceAll('\\', '/')}` : + `file:${absolutePath}`; + + const errors = [{ + url: fileUrl, + message: 'Test error', + type: 'error', + lastLine: 1, + lastColumn: 1 + }]; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.ok(path.isAbsolute(result[0].file) === false, 'File path should be relative'); + }); + + it('should convert to absolute paths when absoluteFilePathsForReporter is true', () => { + const absolutePath = path.resolve('test/fixtures/invalid.html'); + const fileUrl = path.sep === '\\' ? + `file:/${absolutePath.replaceAll('\\', '/')}` : + `file:${absolutePath}`; + + const errors = [{ + url: fileUrl, + message: 'Test error', + type: 'error', + lastLine: 1, + lastColumn: 1 + }]; + const config = { + absoluteFilePathsForReporter: true, + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.ok(path.isAbsolute(result[0].file), 'File path should be absolute'); + }); + }); + + describe('message filtering with ignore', () => { + it('should filter messages matching string ignore rule', () => { + const errors = [ + { + url: 'file:/test.html', + message: 'This should be ignored', + type: 'error', + lastLine: 1, + lastColumn: 1 + }, + { + url: 'file:/test.html', + message: 'This should remain', + type: 'error', + lastLine: 2, + lastColumn: 1 + } + ]; + const config = { + ignore: 'This should be ignored', + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.equal(result[0].message, 'This should remain'); + }); + + it('should filter messages matching regex ignore rule', () => { + const errors = [ + { + url: 'file:/test.html', + message: 'Attribute "data-test" not allowed', + type: 'error', + lastLine: 1, + lastColumn: 1 + }, + { + url: 'file:/test.html', + message: 'This should remain', + type: 'error', + lastLine: 2, + lastColumn: 1 + } + ]; + const config = { + ignore: /attribute "[a-z-]+" not allowed/i, + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.equal(result[0].message, 'This should remain'); + }); + + it('should filter messages matching multiple ignore rules', () => { + const errors = [ + { + url: 'file:/test.html', + message: 'First ignore pattern', + type: 'error', + lastLine: 1, + lastColumn: 1 + }, + { + url: 'file:/test.html', + message: 'Second ignore pattern', + type: 'error', + lastLine: 2, + lastColumn: 1 + }, + { + url: 'file:/test.html', + message: 'This should remain', + type: 'error', + lastLine: 3, + lastColumn: 1 + } + ]; + const config = { + ignore: ['First ignore pattern', /second ignore/i], + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.equal(result[0].message, 'This should remain'); + }); + + it('should handle messages without ignore rules', () => { + const errors = [ + { + url: 'file:/test.html', + message: 'Error message 1', + type: 'error', + lastLine: 1, + lastColumn: 1 + }, + { + url: 'file:/test.html', + message: 'Error message 2', + type: 'error', + lastLine: 2, + lastColumn: 1 + } + ]; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 2); + }); + }); + + describe('edge cases', () => { + it('should handle empty error array', () => { + const errors = []; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 0); + }); + + it('should preserve all message properties', () => { + const errors = [{ + url: 'file:/test.html', + message: 'Test error', + type: 'error', + lastLine: 10, + lastColumn: 20, + firstLine: 8, + firstColumn: 5, + hiliteStart: 100, + hiliteLength: 15 + }]; + const config = { + errorlevels: ['error'] + }; + + const result = processErrorMessages(errors, config); + + assert.equal(result.length, 1); + assert.equal(result[0].message, 'Test error'); + assert.equal(result[0].type, 'error'); + assert.equal(result[0].lastLine, 10); + assert.equal(result[0].lastColumn, 20); + assert.equal(result[0].firstLine, 8); + assert.equal(result[0].firstColumn, 5); + assert.equal(result[0].hiliteStart, 100); + assert.equal(result[0].hiliteLength, 15); + }); + }); +});