Skip to content

Commit 24cdd9c

Browse files
Copilotdmichon-msft
andcommitted
Refactor tests into separate files and improve PathProjectSelectorParser testability
Co-authored-by: dmichon-msft <26827560+dmichon-msft@users.noreply.github.com>
1 parent 2b2fcf9 commit 24cdd9c

8 files changed

Lines changed: 331 additions & 252 deletions

libraries/rush-lib/src/api/test/repo/rush-npm.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,23 @@
2727
{
2828
"packageName": "project1",
2929
"projectFolder": "project1",
30-
"reviewCategory": "third-party"
30+
"reviewCategory": "third-party",
31+
"tags": ["frontend", "ui"]
3132
},
3233

3334
{
3435
"packageName": "project2",
3536
"projectFolder": "project2",
3637
"reviewCategory": "third-party",
37-
"skipRushCheck": true
38+
"skipRushCheck": true,
39+
"tags": ["backend"]
3840
},
3941

4042
{
4143
"packageName": "project3",
4244
"projectFolder": "project3",
43-
"reviewCategory": "prototype"
45+
"reviewCategory": "prototype",
46+
"tags": ["frontend"]
4447
}
4548
]
4649
}

libraries/rush-lib/src/logic/selectors/PathProjectSelectorParser.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import { RushConstants } from '../RushConstants';
1313

1414
export class PathProjectSelectorParser implements ISelectorParser<RushConfigurationProject> {
1515
private readonly _rushConfiguration: RushConfiguration;
16+
private readonly _workingDirectory: string;
1617

17-
public constructor(rushConfiguration: RushConfiguration) {
18+
public constructor(rushConfiguration: RushConfiguration, workingDirectory?: string) {
1819
this._rushConfiguration = rushConfiguration;
20+
this._workingDirectory = workingDirectory ?? process.cwd();
1921
}
2022

2123
public async evaluateSelectorAsync({
@@ -24,7 +26,7 @@ export class PathProjectSelectorParser implements ISelectorParser<RushConfigurat
2426
parameterName
2527
}: IEvaluateSelectorOptions): Promise<Iterable<RushConfigurationProject>> {
2628
// Resolve the input path against the working directory
27-
const absolutePath: string = nodePath.resolve(process.cwd(), unscopedSelector);
29+
const absolutePath: string = nodePath.resolve(this._workingDirectory, unscopedSelector);
2830

2931
// Relativize it to the rushJsonFolder
3032
const relativePath: string = nodePath.relative(this._rushConfiguration.rushJsonFolder, absolutePath);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import * as path from 'node:path';
5+
6+
import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal';
7+
8+
import { RushConfiguration } from '../../../api/RushConfiguration';
9+
import { NamedProjectSelectorParser } from '../NamedProjectSelectorParser';
10+
11+
describe(NamedProjectSelectorParser.name, () => {
12+
let rushConfiguration: RushConfiguration;
13+
let terminal: Terminal;
14+
let terminalProvider: StringBufferTerminalProvider;
15+
let parser: NamedProjectSelectorParser;
16+
17+
beforeEach(() => {
18+
const rushJsonFile: string = path.resolve(__dirname, '../../../api/test/repo/rush-npm.json');
19+
rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFile);
20+
terminalProvider = new StringBufferTerminalProvider();
21+
terminal = new Terminal(terminalProvider);
22+
parser = new NamedProjectSelectorParser(rushConfiguration);
23+
});
24+
25+
it('should select a project by exact package name', async () => {
26+
const result = await parser.evaluateSelectorAsync({
27+
unscopedSelector: 'project1',
28+
terminal,
29+
parameterName: '--only'
30+
});
31+
32+
const projects = Array.from(result);
33+
expect(projects).toHaveLength(1);
34+
expect(projects[0].packageName).toBe('project1');
35+
});
36+
37+
it('should throw error for non-existent project', async () => {
38+
await expect(
39+
parser.evaluateSelectorAsync({
40+
unscopedSelector: 'nonexistent',
41+
terminal,
42+
parameterName: '--only'
43+
})
44+
).rejects.toThrow();
45+
});
46+
47+
it('should provide completions for all projects', () => {
48+
const completions = Array.from(parser.getCompletions());
49+
expect(completions).toContain('project1');
50+
expect(completions).toContain('project2');
51+
expect(completions).toContain('project3');
52+
});
53+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import * as path from 'node:path';
5+
6+
import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal';
7+
8+
import { RushConfiguration } from '../../../api/RushConfiguration';
9+
import { PathProjectSelectorParser } from '../PathProjectSelectorParser';
10+
11+
describe(PathProjectSelectorParser.name, () => {
12+
let rushConfiguration: RushConfiguration;
13+
let terminal: Terminal;
14+
let terminalProvider: StringBufferTerminalProvider;
15+
let parser: PathProjectSelectorParser;
16+
17+
beforeEach(() => {
18+
const rushJsonFile: string = path.resolve(__dirname, '../../../api/test/repo/rush-npm.json');
19+
rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFile);
20+
terminalProvider = new StringBufferTerminalProvider();
21+
terminal = new Terminal(terminalProvider);
22+
parser = new PathProjectSelectorParser(rushConfiguration, rushConfiguration.rushJsonFolder);
23+
});
24+
25+
it('should select a project by exact path', async () => {
26+
const result = await parser.evaluateSelectorAsync({
27+
unscopedSelector: 'project1',
28+
terminal,
29+
parameterName: '--only'
30+
});
31+
32+
const projects = Array.from(result);
33+
expect(projects).toHaveLength(1);
34+
expect(projects[0].packageName).toBe('project1');
35+
});
36+
37+
it('should select a project by path within the project', async () => {
38+
const result = await parser.evaluateSelectorAsync({
39+
unscopedSelector: 'project1/src/index.ts',
40+
terminal,
41+
parameterName: '--only'
42+
});
43+
44+
const projects = Array.from(result);
45+
expect(projects).toHaveLength(1);
46+
expect(projects[0].packageName).toBe('project1');
47+
});
48+
49+
it('should select multiple projects from a parent directory', async () => {
50+
const result = await parser.evaluateSelectorAsync({
51+
unscopedSelector: '.',
52+
terminal,
53+
parameterName: '--only'
54+
});
55+
56+
const projects = Array.from(result);
57+
expect(projects.length).toBeGreaterThan(0);
58+
// Should include all projects in the test repo
59+
const packageNames = projects.map((p) => p.packageName).sort();
60+
expect(packageNames).toContain('project1');
61+
expect(packageNames).toContain('project2');
62+
expect(packageNames).toContain('project3');
63+
});
64+
65+
it('should select project from specified directory', async () => {
66+
const project1Path = path.join(rushConfiguration.rushJsonFolder, 'project1');
67+
const parserWithCustomCwd = new PathProjectSelectorParser(rushConfiguration, project1Path);
68+
69+
const result = await parserWithCustomCwd.evaluateSelectorAsync({
70+
unscopedSelector: '.',
71+
terminal,
72+
parameterName: '--only'
73+
});
74+
75+
const projects = Array.from(result);
76+
expect(projects).toHaveLength(1);
77+
expect(projects[0].packageName).toBe('project1');
78+
});
79+
80+
it('should handle absolute paths', async () => {
81+
const absolutePath = path.join(rushConfiguration.rushJsonFolder, 'project2');
82+
83+
const result = await parser.evaluateSelectorAsync({
84+
unscopedSelector: absolutePath,
85+
terminal,
86+
parameterName: '--only'
87+
});
88+
89+
const projects = Array.from(result);
90+
expect(projects).toHaveLength(1);
91+
expect(projects[0].packageName).toBe('project2');
92+
});
93+
94+
it('should throw error for paths that do not match any project', async () => {
95+
await expect(
96+
parser.evaluateSelectorAsync({
97+
unscopedSelector: 'nonexistent/path',
98+
terminal,
99+
parameterName: '--only'
100+
})
101+
).rejects.toThrow();
102+
});
103+
104+
it('should handle paths outside workspace', async () => {
105+
// Paths outside the workspace should not match any project and throw
106+
await expect(
107+
parser.evaluateSelectorAsync({
108+
unscopedSelector: '../outside',
109+
terminal,
110+
parameterName: '--only'
111+
})
112+
).rejects.toThrow();
113+
});
114+
115+
it('should return empty completions', () => {
116+
const completions = Array.from(parser.getCompletions());
117+
expect(completions).toHaveLength(0);
118+
});
119+
});

0 commit comments

Comments
 (0)