Skip to content

Commit 9b08aa0

Browse files
Fix absolute path check for Windows (#15235)
1 parent 9815c8d commit 9b08aa0

File tree

2 files changed

+81
-44
lines changed

2 files changed

+81
-44
lines changed

packages/jest-pattern/src/TestPathPatterns.ts

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {escapePathForRegex, replacePathSepForRegex} from 'jest-regex-util';
8+
import * as path from 'path';
9+
import {replacePathSepForRegex} from 'jest-regex-util';
910

1011
export class TestPathPatterns {
1112
constructor(readonly patterns: Array<string>) {}
@@ -58,45 +59,13 @@ export type TestPathPatternsExecutorOptions = {
5859
};
5960

6061
export class TestPathPatternsExecutor {
61-
private _regexString: string | null = null;
62-
6362
constructor(
6463
readonly patterns: TestPathPatterns,
6564
private readonly options: TestPathPatternsExecutorOptions,
6665
) {}
6766

68-
private get regexString(): string {
69-
if (this._regexString !== null) {
70-
return this._regexString;
71-
}
72-
73-
const rootDir = this.options.rootDir.replace(/\/*$/, '/');
74-
const rootDirRegex = escapePathForRegex(rootDir);
75-
76-
const regexString = this.patterns.patterns
77-
.map(p => {
78-
// absolute paths passed on command line should stay same
79-
if (p.startsWith('/')) {
80-
return p;
81-
}
82-
83-
// explicit relative paths should resolve against rootDir
84-
if (p.startsWith('./')) {
85-
return p.replace(/^\.\//, rootDirRegex);
86-
}
87-
88-
// all other patterns should only match the relative part of the test
89-
return `${rootDirRegex}(.*)?${p}`;
90-
})
91-
.map(replacePathSepForRegex)
92-
.join('|');
93-
94-
this._regexString = regexString;
95-
return regexString;
96-
}
97-
98-
private toRegex(): RegExp {
99-
return new RegExp(this.regexString, 'i');
67+
private toRegex(s: string): RegExp {
68+
return new RegExp(s, 'i');
10069
}
10170

10271
/**
@@ -111,7 +80,9 @@ export class TestPathPatternsExecutor {
11180
*/
11281
isValid(): boolean {
11382
try {
114-
this.toRegex();
83+
for (const p of this.patterns.patterns) {
84+
this.toRegex(p);
85+
}
11586
return true;
11687
} catch {
11788
return false;
@@ -123,8 +94,29 @@ export class TestPathPatternsExecutor {
12394
*
12495
* Throws an error if the patterns form an invalid regex (see `validate`).
12596
*/
126-
isMatch(path: string): boolean {
127-
return this.toRegex().test(path);
97+
isMatch(absPath: string): boolean {
98+
const relPath = path.relative(this.options.rootDir || '/', absPath);
99+
100+
if (this.patterns.patterns.length === 0) {
101+
return true;
102+
}
103+
104+
for (const p of this.patterns.patterns) {
105+
const pathToTest = path.isAbsolute(p) ? absPath : relPath;
106+
107+
// special case: ./foo.spec.js (and .\foo.spec.js on Windows) should
108+
// match /^foo.spec.js/ after stripping root dir
109+
let regexStr = p.replace(/^\.\//, '^');
110+
if (path.sep === '\\') {
111+
regexStr = regexStr.replace(/^\.\\/, '^');
112+
}
113+
114+
regexStr = replacePathSepForRegex(regexStr);
115+
if (this.toRegex(regexStr).test(pathToTest)) {
116+
return true;
117+
}
118+
}
119+
return false;
128120
}
129121

130122
/**

packages/jest-pattern/src/__tests__/TestPathPatterns.test.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,44 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import type * as path from 'path';
8+
import * as path from 'path';
99
import {
1010
TestPathPatterns,
1111
TestPathPatternsExecutor,
1212
type TestPathPatternsExecutorOptions,
1313
} from '../TestPathPatterns';
1414

1515
const mockSep: jest.Mock<() => string> = jest.fn();
16+
const mockIsAbsolute: jest.Mock<(p: string) => boolean> = jest.fn();
17+
const mockRelative: jest.Mock<(from: string, to: string) => string> = jest.fn();
1618
jest.mock('path', () => {
19+
const actualPath = jest.requireActual('path');
1720
return {
18-
...jest.requireActual('path'),
21+
...actualPath,
22+
isAbsolute(p) {
23+
return mockIsAbsolute(p) || actualPath.isAbsolute(p);
24+
},
25+
relative(from, to) {
26+
return mockRelative(from, to) || actualPath.relative(from, to);
27+
},
1928
get sep() {
20-
return mockSep() || '/';
29+
return mockSep() || actualPath.sep;
2130
},
2231
} as typeof path;
2332
});
33+
const forcePosix = () => {
34+
mockSep.mockReturnValue(path.posix.sep);
35+
mockIsAbsolute.mockImplementation(path.posix.isAbsolute);
36+
mockRelative.mockImplementation(path.posix.relative);
37+
};
38+
const forceWindows = () => {
39+
mockSep.mockReturnValue(path.win32.sep);
40+
mockIsAbsolute.mockImplementation(path.win32.isAbsolute);
41+
mockRelative.mockImplementation(path.win32.relative);
42+
};
2443
beforeEach(() => {
2544
jest.resetAllMocks();
45+
forcePosix();
2646
});
2747

2848
const config = {rootDir: ''};
@@ -124,6 +144,22 @@ describe('TestPathPatternsExecutor', () => {
124144
expect(testPathPatterns.isMatch('/a/b/c')).toBe(true);
125145
});
126146

147+
it('returns true for explicit relative path for Windows with ./', () => {
148+
forceWindows();
149+
const testPathPatterns = makeExecutor(['./b/c'], {
150+
rootDir: 'C:\\a',
151+
});
152+
expect(testPathPatterns.isMatch('C:\\a\\b\\c')).toBe(true);
153+
});
154+
155+
it('returns true for explicit relative path for Windows with .\\', () => {
156+
forceWindows();
157+
const testPathPatterns = makeExecutor(['.\\b\\c'], {
158+
rootDir: 'C:\\a',
159+
});
160+
expect(testPathPatterns.isMatch('C:\\a\\b\\c')).toBe(true);
161+
});
162+
127163
it('returns true for partial file match', () => {
128164
const testPathPatterns = makeExecutor(['aaa'], config);
129165
expect(testPathPatterns.isMatch('/foo/..aaa..')).toBe(true);
@@ -158,12 +194,21 @@ describe('TestPathPatternsExecutor', () => {
158194
});
159195

160196
it('matches absolute paths regardless of rootDir', () => {
197+
forcePosix();
161198
const testPathPatterns = makeExecutor(['/a/b'], {
162199
rootDir: '/foo/bar',
163200
});
164201
expect(testPathPatterns.isMatch('/a/b')).toBe(true);
165202
});
166203

204+
it('matches absolute paths for Windows', () => {
205+
forceWindows();
206+
const testPathPatterns = makeExecutor(['C:\\a\\b'], {
207+
rootDir: 'C:\\foo\\bar',
208+
});
209+
expect(testPathPatterns.isMatch('C:\\a\\b')).toBe(true);
210+
});
211+
167212
it('returns true if match any paths', () => {
168213
const testPathPatterns = makeExecutor(['a/b', 'c/d'], config);
169214

@@ -175,15 +220,15 @@ describe('TestPathPatternsExecutor', () => {
175220
});
176221

177222
it('does not normalize Windows paths on POSIX', () => {
178-
mockSep.mockReturnValue('/');
223+
forcePosix();
179224
const testPathPatterns = makeExecutor(['a\\z', 'a\\\\z'], config);
180225
expect(testPathPatterns.isMatch('/foo/a/z')).toBe(false);
181226
});
182227

183228
it('normalizes paths for Windows', () => {
184-
mockSep.mockReturnValue('\\');
229+
forceWindows();
185230
const testPathPatterns = makeExecutor(['a/b'], config);
186-
expect(testPathPatterns.isMatch('\\foo\\a\\b')).toBe(true);
231+
expect(testPathPatterns.isMatch('C:\\foo\\a\\b')).toBe(true);
187232
});
188233
});
189234
});

0 commit comments

Comments
 (0)