Skip to content

Commit 21606ce

Browse files
committed
test(@angular/build): add unit tests for test discovery logic
The `test-discovery.ts` file contains critical logic for finding test files based on user-defined glob patterns and static paths. This logic was previously not fully tested, making it difficult to refactor safely and prone to regressions. This commit introduces a unit test suite for the `findTests` and `getTestEntrypoints` functions. It uses a mocked file system to cover various scenarios, including: - Dynamic glob pattern resolution - Static directory and file path expansion - Correct application of exclusion patterns - De-duplication of results
1 parent 570c60a commit 21606ce

2 files changed

Lines changed: 183 additions & 97 deletions

File tree

packages/angular/build/src/builders/karma/find-tests_spec.ts

Lines changed: 0 additions & 97 deletions
This file was deleted.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { promises as fs } from 'node:fs';
10+
import * as path from 'node:path';
11+
import * as tinyglobby from 'tinyglobby';
12+
import { findTests, getTestEntrypoints } from '../test-discovery';
13+
14+
const WORKSPACE_ROOT = '/my/workspace/root';
15+
const PROJECT_SOURCE_ROOT = '/my/workspace/root/src-root';
16+
17+
describe('findTests', () => {
18+
let globSpy: jasmine.Spy;
19+
let statSpy: jasmine.Spy;
20+
let accessSpy: jasmine.Spy;
21+
22+
beforeEach(() => {
23+
globSpy = spyOn(tinyglobby, 'glob').and.returnValue(Promise.resolve([]));
24+
statSpy = spyOn(fs, 'stat').and.returnValue(Promise.reject(new Error('Not a file.')));
25+
accessSpy = spyOn(fs, 'access').and.returnValue(
26+
Promise.reject(new Error('File does not exist.')),
27+
);
28+
});
29+
30+
function posixPath(p: string): string {
31+
return p.replace(/\\/g, '/');
32+
}
33+
34+
it('finds tests using a simple glob', async () => {
35+
globSpy.and.returnValue(Promise.resolve(['/my/workspace/root/src-root/app/app.spec.ts']));
36+
37+
const files = await findTests(['**/*.spec.ts'], [], WORKSPACE_ROOT, PROJECT_SOURCE_ROOT);
38+
expect(files).toEqual(['/my/workspace/root/src-root/app/app.spec.ts']);
39+
expect(posixPath(globSpy.calls.mostRecent().args[0][0])).toBe('**/*.spec.ts');
40+
});
41+
42+
it('excludes files using an exclude pattern', async () => {
43+
globSpy.and.returnValue(Promise.resolve(['/my/workspace/root/src-root/app/app.spec.ts']));
44+
45+
await findTests(['**/*.spec.ts'], ['**/e2e/**'], WORKSPACE_ROOT, PROJECT_SOURCE_ROOT);
46+
47+
expect(globSpy.calls.mostRecent().args[1].ignore).toContain('**/e2e/**');
48+
});
49+
50+
it('expands a static directory path', async () => {
51+
statSpy.and.callFake(async (p: string) => ({ isDirectory: () => p.endsWith('app') }));
52+
53+
await findTests(['app'], [], WORKSPACE_ROOT, PROJECT_SOURCE_ROOT);
54+
55+
expect(posixPath(globSpy.calls.mostRecent().args[0][0])).toBe('app/**/*.spec.@(ts|tsx)');
56+
});
57+
58+
it('resolves a static file path to its spec file', async () => {
59+
accessSpy.and.callFake(async (p: string) => {
60+
if (p.endsWith('app.component.spec.ts')) {
61+
return;
62+
}
63+
throw new Error('File does not exist.');
64+
});
65+
66+
const files = await findTests(
67+
['app/app.component.ts'],
68+
[],
69+
WORKSPACE_ROOT,
70+
PROJECT_SOURCE_ROOT,
71+
);
72+
73+
expect(files).toEqual([path.join(PROJECT_SOURCE_ROOT, 'app/app.component.spec.ts')]);
74+
expect(globSpy).not.toHaveBeenCalled();
75+
});
76+
77+
it('deduplicates files found by multiple patterns', async () => {
78+
globSpy.and.callFake(async (patterns: string[]) => {
79+
if (patterns.includes('src/app/**/*.spec.ts')) {
80+
return ['/my/workspace/root/src-root/app/app.spec.ts'];
81+
}
82+
if (patterns.includes('src/lib/**/*.spec.ts')) {
83+
return ['/my/workspace/root/src-root/app/app.spec.ts'];
84+
}
85+
return [];
86+
});
87+
88+
const files = await findTests(
89+
['src/app/**/*.spec.ts', 'src/lib/**/*.spec.ts'],
90+
[],
91+
WORKSPACE_ROOT,
92+
PROJECT_SOURCE_ROOT,
93+
);
94+
95+
expect(files).toEqual(['/my/workspace/root/src-root/app/app.spec.ts']);
96+
});
97+
});
98+
99+
describe('getTestEntrypoints', () => {
100+
const UNIX_ENTRYPOINTS_OPTIONS = {
101+
workspaceRoot: '/my/workspace/root',
102+
projectSourceRoot: '/my/workspace/root/src-root',
103+
};
104+
105+
const WINDOWS_ENTRYPOINTS_OPTIONS = {
106+
workspaceRoot: 'C:\\my\\workspace\\root',
107+
projectSourceRoot: 'C:\\my\\workspace\\root\\src-root',
108+
};
109+
110+
for (const options of [UNIX_ENTRYPOINTS_OPTIONS, WINDOWS_ENTRYPOINTS_OPTIONS]) {
111+
describe(`with path separator "${path.sep}"`, () => {
112+
function joinWithSeparator(base: string, rel: string) {
113+
return path.join(base, rel);
114+
}
115+
116+
function getEntrypoints(workspaceRelative: string[], sourceRootRelative: string[] = []) {
117+
return getTestEntrypoints(
118+
[
119+
...workspaceRelative.map((p) => joinWithSeparator(options.workspaceRoot, p)),
120+
...sourceRootRelative.map((p) => joinWithSeparator(options.projectSourceRoot, p)),
121+
],
122+
options,
123+
);
124+
}
125+
126+
it('returns an empty map without test files', () => {
127+
expect(getEntrypoints([])).toEqual(new Map());
128+
});
129+
130+
it('strips workspace root and/or project source root', () => {
131+
expect(getEntrypoints(['a/b.spec.js'], ['c/d.spec.js'])).toEqual(
132+
new Map<string, string>([
133+
['spec-a-b.spec', joinWithSeparator(options.workspaceRoot, 'a/b.spec.js')],
134+
['spec-c-d.spec', joinWithSeparator(options.projectSourceRoot, 'c/d.spec.js')],
135+
]),
136+
);
137+
});
138+
139+
it('adds unique prefixes to distinguish between similar names', () => {
140+
expect(getEntrypoints(['a/b/c/d.spec.js', 'a-b/c/d.spec.js'], ['a/b-c/d.spec.js'])).toEqual(
141+
new Map<string, string>([
142+
['spec-a-b-c-d.spec', joinWithSeparator(options.workspaceRoot, 'a/b/c/d.spec.js')],
143+
['spec-a-b-c-d-2.spec', joinWithSeparator(options.workspaceRoot, 'a-b/c/d.spec.js')],
144+
[
145+
'spec-a-b-c-d-3.spec',
146+
joinWithSeparator(options.projectSourceRoot, 'a/b-c/d.spec.js'),
147+
],
148+
]),
149+
);
150+
});
151+
152+
describe('with removeTestExtension enabled', () => {
153+
function getEntrypoints(workspaceRelative: string[], sourceRootRelative: string[] = []) {
154+
return getTestEntrypoints(
155+
[
156+
...workspaceRelative.map((p) => joinWithSeparator(options.workspaceRoot, p)),
157+
...sourceRootRelative.map((p) => joinWithSeparator(options.projectSourceRoot, p)),
158+
],
159+
{ ...options, removeTestExtension: true },
160+
);
161+
}
162+
163+
it('removes .spec extension', () => {
164+
expect(getEntrypoints(['a/b.spec.js'], ['c/d.spec.js'])).toEqual(
165+
new Map<string, string>([
166+
['spec-a-b', joinWithSeparator(options.workspaceRoot, 'a/b.spec.js')],
167+
['spec-c-d', joinWithSeparator(options.projectSourceRoot, 'c/d.spec.js')],
168+
]),
169+
);
170+
});
171+
172+
it('removes .test extension', () => {
173+
expect(getEntrypoints(['a/b.test.js'], ['c/d.test.js'])).toEqual(
174+
new Map<string, string>([
175+
['spec-a-b', joinWithSeparator(options.workspaceRoot, 'a/b.test.js')],
176+
['spec-c-d', joinWithSeparator(options.projectSourceRoot, 'c/d.test.js')],
177+
]),
178+
);
179+
});
180+
});
181+
});
182+
}
183+
});

0 commit comments

Comments
 (0)