From 5023a4dc7eaee44fccbd5e6caf3f0d47f501956f Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 20 Oct 2025 07:42:01 -0400 Subject: [PATCH] test: add comprehensive glob-utils tests - Test all glob pattern types: *, **, ?, [abc], {a,b} - Test edge cases: unclosed brackets, unclosed braces - Test all public methods: matches(), matchesIncludes(), matchesExcludes(), shouldInclude() - Test special regex character escaping: . / ( ) + ^ $ | \ - Test complex pattern combinations and real-world scenarios - Achieve 100% coverage on glob-utils.ts (52 tests) --- tests/glob-utils.test.ts | 325 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 tests/glob-utils.test.ts diff --git a/tests/glob-utils.test.ts b/tests/glob-utils.test.ts new file mode 100644 index 0000000..8be69ca --- /dev/null +++ b/tests/glob-utils.test.ts @@ -0,0 +1,325 @@ +import { GlobUtils } from '../src/utils/glob-utils'; + +describe('GlobUtils', () => { + describe('matches()', () => { + describe('* pattern (matches any chars except /)', () => { + test('matches single directory wildcard', () => { + expect(GlobUtils.matches('file.md', '*.md')).toBe(true); + expect(GlobUtils.matches('document.txt', '*.md')).toBe(false); + expect(GlobUtils.matches('folder/file.md', '*.md')).toBe(false); + }); + + test('matches wildcard in middle of pattern', () => { + expect(GlobUtils.matches('test-file.md', 'test-*.md')).toBe(true); + expect(GlobUtils.matches('test-document.md', 'test-*.md')).toBe(true); + expect(GlobUtils.matches('other-file.md', 'test-*.md')).toBe(false); + }); + + test('does not match across directory separators', () => { + expect(GlobUtils.matches('folder/file.md', '*/file.md')).toBe(true); + expect(GlobUtils.matches('folder/subfolder/file.md', '*/file.md')).toBe(false); + }); + + test('matches multiple wildcards', () => { + expect(GlobUtils.matches('a-test-file.md', '*-*-*.md')).toBe(true); + expect(GlobUtils.matches('test.md', '*.*')).toBe(true); + }); + }); + + describe('** pattern (matches any chars including /)', () => { + test('matches across directory separators', () => { + expect(GlobUtils.matches('folder/file.md', '**/*.md')).toBe(true); + expect(GlobUtils.matches('folder/subfolder/file.md', '**/*.md')).toBe(true); + expect(GlobUtils.matches('file.md', '**/*.md')).toBe(true); + }); + + test('matches ** in middle of pattern', () => { + expect(GlobUtils.matches('src/utils/helper.ts', 'src/**/helper.ts')).toBe(true); + expect(GlobUtils.matches('src/helper.ts', 'src/**/helper.ts')).toBe(true); + expect(GlobUtils.matches('src/deeply/nested/path/helper.ts', 'src/**/helper.ts')).toBe(true); + }); + + test('handles ** with trailing slash', () => { + expect(GlobUtils.matches('folder/file.md', '**/file.md')).toBe(true); + expect(GlobUtils.matches('a/b/c/file.md', '**/file.md')).toBe(true); + }); + + test('matches ** alone', () => { + expect(GlobUtils.matches('anything/path/file.md', '**')).toBe(true); + expect(GlobUtils.matches('file.md', '**')).toBe(true); + }); + }); + + describe('? pattern (matches single char except /)', () => { + test('matches single character', () => { + expect(GlobUtils.matches('file1.md', 'file?.md')).toBe(true); + expect(GlobUtils.matches('file2.md', 'file?.md')).toBe(true); + expect(GlobUtils.matches('file12.md', 'file?.md')).toBe(false); + expect(GlobUtils.matches('file.md', 'file?.md')).toBe(false); + }); + + test('does not match directory separator', () => { + expect(GlobUtils.matches('file/x', 'file?x')).toBe(false); + expect(GlobUtils.matches('fileax', 'file?x')).toBe(true); + }); + + test('matches multiple ? patterns', () => { + expect(GlobUtils.matches('ab.md', '??.md')).toBe(true); + expect(GlobUtils.matches('a.md', '??.md')).toBe(false); + expect(GlobUtils.matches('abc.md', '??.md')).toBe(false); + }); + }); + + describe('[abc] pattern (character class)', () => { + test('matches character in set', () => { + expect(GlobUtils.matches('filea.md', 'file[abc].md')).toBe(true); + expect(GlobUtils.matches('fileb.md', 'file[abc].md')).toBe(true); + expect(GlobUtils.matches('filec.md', 'file[abc].md')).toBe(true); + expect(GlobUtils.matches('filed.md', 'file[abc].md')).toBe(false); + }); + + test('matches character ranges', () => { + expect(GlobUtils.matches('file1.md', 'file[0-9].md')).toBe(true); + expect(GlobUtils.matches('file5.md', 'file[0-9].md')).toBe(true); + expect(GlobUtils.matches('filea.md', 'file[0-9].md')).toBe(false); + }); + + test('handles unclosed bracket as literal', () => { + expect(GlobUtils.matches('[abc', '[abc')).toBe(true); + expect(GlobUtils.matches('xabc', '[abc')).toBe(false); + }); + }); + + describe('{a,b} pattern (alternatives)', () => { + test('matches any alternative', () => { + expect(GlobUtils.matches('file.md', 'file.{md,txt}')).toBe(true); + expect(GlobUtils.matches('file.txt', 'file.{md,txt}')).toBe(true); + expect(GlobUtils.matches('file.pdf', 'file.{md,txt}')).toBe(false); + }); + + test('matches complex alternatives', () => { + expect(GlobUtils.matches('src/test.ts', '{src,dist}/{test,main}.ts')).toBe(true); + expect(GlobUtils.matches('dist/main.ts', '{src,dist}/{test,main}.ts')).toBe(true); + expect(GlobUtils.matches('lib/test.ts', '{src,dist}/{test,main}.ts')).toBe(false); + }); + + test('handles unclosed brace as literal', () => { + expect(GlobUtils.matches('{abc', '{abc')).toBe(true); + expect(GlobUtils.matches('xabc', '{abc')).toBe(false); + }); + + test('escapes special chars in alternatives', () => { + expect(GlobUtils.matches('file.test', 'file.{test,prod}')).toBe(true); + expect(GlobUtils.matches('file.prod', 'file.{test,prod}')).toBe(true); + }); + }); + + describe('special regex character escaping', () => { + test('escapes . (dot)', () => { + expect(GlobUtils.matches('file.md', 'file.md')).toBe(true); + expect(GlobUtils.matches('fileXmd', 'file.md')).toBe(false); + }); + + test('escapes / (slash)', () => { + expect(GlobUtils.matches('folder/file.md', 'folder/file.md')).toBe(true); + }); + + test('escapes ( and )', () => { + expect(GlobUtils.matches('file(1).md', 'file(1).md')).toBe(true); + }); + + test('escapes +', () => { + expect(GlobUtils.matches('file+test.md', 'file+test.md')).toBe(true); + }); + + test('escapes ^', () => { + expect(GlobUtils.matches('file^test.md', 'file^test.md')).toBe(true); + }); + + test('escapes $', () => { + expect(GlobUtils.matches('file$test.md', 'file$test.md')).toBe(true); + }); + + test('escapes |', () => { + expect(GlobUtils.matches('file|test.md', 'file|test.md')).toBe(true); + }); + + test('escapes \\ (backslash)', () => { + expect(GlobUtils.matches('file\\test.md', 'file\\test.md')).toBe(true); + }); + }); + + describe('complex pattern combinations', () => { + test('combines multiple pattern types', () => { + expect(GlobUtils.matches('src/utils/test-file.ts', 'src/**/*-*.{ts,js}')).toBe(true); + expect(GlobUtils.matches('src/nested/my-helper.js', 'src/**/*-*.{ts,js}')).toBe(true); + expect(GlobUtils.matches('src/file.ts', 'src/**/*-*.{ts,js}')).toBe(false); + }); + + test('matches real-world patterns', () => { + expect(GlobUtils.matches('tests/unit/helper.test.ts', 'tests/**/*.test.ts')).toBe(true); + expect(GlobUtils.matches('src/index.ts', 'tests/**/*.test.ts')).toBe(false); + }); + }); + + describe('edge cases', () => { + test('matches empty pattern with empty string', () => { + expect(GlobUtils.matches('', '')).toBe(true); + }); + + test('does not match non-empty with empty pattern', () => { + expect(GlobUtils.matches('file.md', '')).toBe(false); + }); + + test('handles patterns with no wildcards', () => { + expect(GlobUtils.matches('exact/path/file.md', 'exact/path/file.md')).toBe(true); + expect(GlobUtils.matches('other/path/file.md', 'exact/path/file.md')).toBe(false); + }); + }); + }); + + describe('matchesIncludes()', () => { + test('returns true when includes is undefined', () => { + expect(GlobUtils.matchesIncludes('any/path.md', undefined)).toBe(true); + }); + + test('returns true when includes is empty array', () => { + expect(GlobUtils.matchesIncludes('any/path.md', [])).toBe(true); + }); + + test('returns true when path matches any include pattern', () => { + const includes = ['*.md', '*.txt']; + expect(GlobUtils.matchesIncludes('file.md', includes)).toBe(true); + expect(GlobUtils.matchesIncludes('file.txt', includes)).toBe(true); + }); + + test('returns false when path matches no include patterns', () => { + const includes = ['*.md', '*.txt']; + expect(GlobUtils.matchesIncludes('file.pdf', includes)).toBe(false); + }); + + test('matches with complex patterns', () => { + const includes = ['src/**/*.ts', 'tests/**/*.test.js']; + expect(GlobUtils.matchesIncludes('src/utils/helper.ts', includes)).toBe(true); + expect(GlobUtils.matchesIncludes('tests/unit/file.test.js', includes)).toBe(true); + expect(GlobUtils.matchesIncludes('docs/readme.md', includes)).toBe(false); + }); + + test('stops at first match (optimization check)', () => { + const includes = ['*.md', '*.txt', '*.pdf']; + // Should match first pattern and not need to check others + expect(GlobUtils.matchesIncludes('file.md', includes)).toBe(true); + }); + }); + + describe('matchesExcludes()', () => { + test('returns false when excludes is undefined', () => { + expect(GlobUtils.matchesExcludes('any/path.md', undefined)).toBe(false); + }); + + test('returns false when excludes is empty array', () => { + expect(GlobUtils.matchesExcludes('any/path.md', [])).toBe(false); + }); + + test('returns true when path matches any exclude pattern', () => { + const excludes = ['*.tmp', 'node_modules/**']; + expect(GlobUtils.matchesExcludes('file.tmp', excludes)).toBe(true); + expect(GlobUtils.matchesExcludes('node_modules/package/index.js', excludes)).toBe(true); + }); + + test('returns false when path matches no exclude patterns', () => { + const excludes = ['*.tmp', 'node_modules/**']; + expect(GlobUtils.matchesExcludes('src/file.ts', excludes)).toBe(false); + }); + + test('matches with complex patterns', () => { + const excludes = ['**/*.test.ts', '**/dist/**', '.git/**']; + expect(GlobUtils.matchesExcludes('src/file.test.ts', excludes)).toBe(true); + expect(GlobUtils.matchesExcludes('build/dist/main.js', excludes)).toBe(true); + expect(GlobUtils.matchesExcludes('.git/config', excludes)).toBe(true); + expect(GlobUtils.matchesExcludes('src/main.ts', excludes)).toBe(false); + }); + + test('stops at first match (optimization check)', () => { + const excludes = ['*.tmp', '*.bak', '*.old']; + // Should match first pattern and not need to check others + expect(GlobUtils.matchesExcludes('file.tmp', excludes)).toBe(true); + }); + }); + + describe('shouldInclude()', () => { + test('returns true when no includes or excludes specified', () => { + expect(GlobUtils.shouldInclude('any/path.md')).toBe(true); + expect(GlobUtils.shouldInclude('any/path.md', undefined, undefined)).toBe(true); + }); + + test('returns true when matches includes and no excludes', () => { + const includes = ['*.md']; + expect(GlobUtils.shouldInclude('file.md', includes)).toBe(true); + }); + + test('returns false when does not match includes', () => { + const includes = ['*.md']; + expect(GlobUtils.shouldInclude('file.txt', includes)).toBe(false); + }); + + test('returns false when matches excludes', () => { + const excludes = ['*.tmp']; + expect(GlobUtils.shouldInclude('file.tmp', undefined, excludes)).toBe(false); + }); + + test('returns false when matches excludes even if matches includes', () => { + const includes = ['*.md']; + const excludes = ['draft-*']; + expect(GlobUtils.shouldInclude('draft-file.md', includes, excludes)).toBe(false); + }); + + test('returns true when matches includes and does not match excludes', () => { + const includes = ['*.md']; + const excludes = ['draft-*']; + expect(GlobUtils.shouldInclude('final-file.md', includes, excludes)).toBe(true); + }); + + test('handles complex real-world scenarios', () => { + const includes = ['src/**/*.ts', 'tests/**/*.ts']; + const excludes = ['**/*.test.ts', '**/dist/**', 'node_modules/**']; + + // Should include: matches includes, not excluded + expect(GlobUtils.shouldInclude('src/utils/helper.ts', includes, excludes)).toBe(true); + + // Should exclude: matches test pattern + expect(GlobUtils.shouldInclude('tests/unit.test.ts', includes, excludes)).toBe(false); + + // Should exclude: in dist folder + expect(GlobUtils.shouldInclude('src/dist/compiled.ts', includes, excludes)).toBe(false); + + // Should exclude: doesn't match includes + expect(GlobUtils.shouldInclude('docs/readme.md', includes, excludes)).toBe(false); + }); + + test('includes take precedence before checking excludes', () => { + const includes = ['src/**']; + const excludes = ['**/*.tmp']; + + // Doesn't match includes, so excluded before exclude patterns checked + expect(GlobUtils.shouldInclude('dist/file.js', includes, excludes)).toBe(false); + + // Matches includes but also matches excludes + expect(GlobUtils.shouldInclude('src/file.tmp', includes, excludes)).toBe(false); + + // Matches includes and doesn't match excludes + expect(GlobUtils.shouldInclude('src/file.js', includes, excludes)).toBe(true); + }); + + test('empty arrays behave correctly', () => { + // Empty includes means include everything + expect(GlobUtils.shouldInclude('any/file.md', [], ['*.tmp'])).toBe(true); + + // Empty excludes means exclude nothing + expect(GlobUtils.shouldInclude('file.md', ['*.md'], [])).toBe(true); + + // Both empty means include everything + expect(GlobUtils.shouldInclude('any/file.md', [], [])).toBe(true); + }); + }); +}); \ No newline at end of file