Skip to content

Commit b1158c7

Browse files
authored
feat: support filter projects via --project option (#520)
1 parent 43e4bbf commit b1158c7

File tree

13 files changed

+206
-4
lines changed

13 files changed

+206
-4
lines changed

e2e/projects/filter.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,23 @@ describe('test projects filter', () => {
2525
expect(logs.find((log) => log.includes('node/test/index'))).toBeFalsy();
2626
expect(logs.find((log) => log.includes('client/test/index'))).toBeTruthy();
2727
});
28+
29+
it('should run test success with project filter', async () => {
30+
const { cli, expectExecSuccess } = await runRstestCli({
31+
command: 'rstest',
32+
args: ['run', '--project', 'node', '--globals'],
33+
options: {
34+
nodeOptions: {
35+
cwd: join(__dirname, 'fixtures'),
36+
},
37+
},
38+
});
39+
40+
await expectExecSuccess();
41+
const logs = cli.stdout.split('\n').filter(Boolean);
42+
43+
// test log print
44+
expect(logs.find((log) => log.includes('node/test/index'))).toBeTruthy();
45+
expect(logs.find((log) => log.includes('client/test/index'))).toBeFalsy();
46+
});
2847
});

packages/core/src/cli/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ const applyCommonOptions = (cli: CAC) => {
3434
.option('--include <include>', 'Match test files')
3535
.option('--exclude <exclude>', 'Exclude files from test')
3636
.option('-u, --update', 'Update snapshot files')
37+
.option(
38+
'--project <name>',
39+
'Run only projects that match the name, can be a full name or wildcards pattern',
40+
)
3741
.option(
3842
'--passWithNoTests',
3943
'Allows the test suite to pass when no files are found',

packages/core/src/cli/init.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { basename, dirname, resolve } from 'pathe';
44
import { type GlobOptions, glob, isDynamicPattern } from 'tinyglobby';
55
import { loadConfig } from '../config';
66
import type { Project, RstestConfig } from '../types';
7-
import { castArray, getAbsolutePath, logger } from '../utils';
7+
import {
8+
castArray,
9+
color,
10+
filterProjects,
11+
getAbsolutePath,
12+
logger,
13+
} from '../utils';
814

915
export type CommonOptions = {
1016
root?: string;
@@ -15,6 +21,7 @@ export type CommonOptions = {
1521
include?: string[];
1622
exclude?: string[];
1723
reporter?: string[];
24+
project?: string[];
1825
passWithNoTests?: boolean;
1926
printConsoleTrace?: boolean;
2027
disableConsoleIntercept?: boolean;
@@ -99,7 +106,7 @@ export async function resolveProjects({
99106
root: string;
100107
options: CommonOptions;
101108
}): Promise<Project[]> {
102-
if (!config.projects || !config.projects.length) {
109+
if (!config.projects) {
103110
return [];
104111
}
105112

@@ -174,7 +181,18 @@ export async function resolveProjects({
174181
configFilePath,
175182
};
176183
}),
177-
);
184+
).then((projects) => filterProjects(projects, options));
185+
186+
if (!projects.length) {
187+
let errorMsg = `No projects found, please make sure you have at least one valid project.
188+
${color.gray('projects:')} ${JSON.stringify(config.projects, null, 2)}`;
189+
190+
if (options.project) {
191+
errorMsg += `\n${color.gray('projectName filter:')} ${JSON.stringify(options.project, null, 2)}`;
192+
}
193+
194+
throw errorMsg;
195+
}
178196

179197
const names = new Set<string>();
180198

packages/core/src/utils/testFiles.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
33
import { createRequire } from 'node:module';
44
import pathe from 'pathe';
55
import { glob } from 'tinyglobby';
6+
import type { Project } from '../types';
67
import { castArray, color, getAbsolutePath, parsePosix } from './helper';
78

89
export const filterFiles = (
@@ -38,6 +39,32 @@ export const filterFiles = (
3839
});
3940
};
4041

42+
export const filterProjects = (
43+
projects: Project[],
44+
options: {
45+
project?: string[];
46+
},
47+
): Project[] => {
48+
if (options.project) {
49+
const regexes = castArray(options.project).map((pattern) => {
50+
// cast wildcard to RegExp, eg. @rstest/*, !@rstest/core
51+
const isNeg = pattern.startsWith('!');
52+
53+
const escaped = (isNeg ? pattern.slice(1) : pattern)
54+
.split('*')
55+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
56+
.join('.*');
57+
return new RegExp(isNeg ? `^(?!${escaped})` : `^${escaped}$`);
58+
});
59+
60+
return projects.filter((proj) =>
61+
regexes.some((re) => re.test(proj.config.name!)),
62+
);
63+
}
64+
65+
return projects;
66+
};
67+
4168
const hasInSourceTestCode = (code: string): boolean =>
4269
code.includes('import.meta.rstest');
4370

packages/core/tests/utils/testFiles.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import path from 'node:path';
2-
import { filterFiles, formatTestEntryName } from '../../src/utils/testFiles';
2+
import {
3+
filterFiles,
4+
filterProjects,
5+
formatTestEntryName,
6+
} from '../../src/utils/testFiles';
37

48
describe('test filterFiles', () => {
59
it('should filter files correctly', () => {
@@ -29,3 +33,59 @@ test('formatTestEntryName', () => {
2933
expect(formatTestEntryName('some.setup.ts')).toBe('some~setup~ts');
3034
expect(formatTestEntryName('some/setup.ts')).toBe('some_setup~ts');
3135
});
36+
37+
test('filterProjects', () => {
38+
const projects = [
39+
{
40+
config: { name: '@rstest/core' },
41+
relativeRoot: 'packages/core',
42+
},
43+
{
44+
config: { name: '@rstest/coverage' },
45+
relativeRoot: 'packages/coverage',
46+
},
47+
{
48+
config: { name: 'react' },
49+
relativeRoot: 'example/react',
50+
},
51+
];
52+
53+
expect(filterProjects(projects, {})).toEqual(projects);
54+
55+
expect(
56+
filterProjects(projects, {
57+
project: ['@rstest/core'],
58+
}),
59+
).toEqual([
60+
{
61+
config: { name: '@rstest/core' },
62+
relativeRoot: 'packages/core',
63+
},
64+
]);
65+
66+
expect(
67+
filterProjects(projects, {
68+
project: ['@rstest/*'],
69+
}),
70+
).toEqual([
71+
{
72+
config: { name: '@rstest/core' },
73+
relativeRoot: 'packages/core',
74+
},
75+
{
76+
config: { name: '@rstest/coverage' },
77+
relativeRoot: 'packages/coverage',
78+
},
79+
]);
80+
81+
expect(
82+
filterProjects(projects, {
83+
project: ['!@rstest/*'],
84+
}),
85+
).toEqual([
86+
{
87+
config: { name: 'react' },
88+
relativeRoot: 'example/react',
89+
},
90+
]);
91+
});

website/docs/en/config/test/name.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,15 @@
44
- **Default:** `rstest`
55

66
The name of the test project.
7+
8+
```ts title="rstest.config.ts"
9+
import { defineConfig } from '@rstest/core';
10+
11+
export default defineConfig({
12+
name: 'node',
13+
});
14+
```
15+
16+
When you define multiple test projects via [projects](/config/test/projects), it's recommended to give each project a unique name so that you can filter specific projects using the [--project](/guide/basic/test-filter#filter-by-project-name) option.
17+
18+
If a project's name is not provided, Rstest will use the `name` field from the current project's `package.json`. If it does not exist, the folder name will be used instead.

website/docs/en/config/test/projects.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ An array of directories, config files, or glob patterns that define multiple tes
77

88
`rstest` will run the tests for each project according to the configuration defined in each project, and the test results from all projects will be combined and displayed.
99

10+
You can filter the specified projects to run by using the [--project](/guide/basic/test-filter#filter-by-project-name) option.
11+
1012
If there is no `projects` field, `rstest` will treat current directory as a single project.
1113

1214
```ts name='rstest.config.ts'

website/docs/en/guide/basic/cli.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Rstest CLI provides several common options that can be used with all commands:
118118
| `-u, --update` | Update snapshot files, see [update](/config/test/update) |
119119
| `--passWithNoTests` | Allows the test suite to pass when no files are found, see [passWithNoTests](/config/test/passWithNoTests) |
120120
| `--printConsoleTrace` | Print console traces when calling any console method, see [printConsoleTrace](/config/test/printConsoleTrace) |
121+
| `--project <name>` | Only run tests for the specified project, see [Filter by project name](/guide/basic/test-filter#filter-by-project-name) |
121122
| `--disableConsoleIntercept` | Disable console intercept, see [disableConsoleIntercept](/config/test/disableConsoleIntercept) |
122123
| `--slowTestThreshold <value>` | The number of milliseconds after which a test or suite is considered slow, see [slowTestThreshold](/config/test/slowTestThreshold) |
123124
| `-t, --testNamePattern <value>` | Run only tests with a name that matches the regex, see [testNamePattern](/config/test/testNamePattern) |

website/docs/en/guide/basic/test-filter.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ rstest --testNamePattern login
8080
rstest -t login
8181
```
8282

83+
## Filter by project name
84+
85+
Rstest supports defining multiple test projects via [projects](/config/test/projects). You can filter to run specific projects with the `--project` option.
86+
87+
For example, to match projects whose [name](/config/test/name) is `@test/a` or `@test/b`:
88+
89+
```bash
90+
rstest --project '@test/a' --project '@test/b'
91+
```
92+
93+
You can also use wildcards to match project names:
94+
95+
```bash
96+
rstest --project '@test/*'
97+
```
98+
99+
You can exclude certain projects by negation:
100+
101+
```bash
102+
rstest --project '!@test/a'
103+
```
104+
83105
## Combined filtering
84106

85107
All filtering methods can be combined. For example:

website/docs/zh/config/test/name.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,15 @@
44
- **默认值:** `rstest`
55

66
测试项目的名称。
7+
8+
```ts title="rstest.config.ts"
9+
import { defineConfig } from '@rstest/core';
10+
11+
export default defineConfig({
12+
name: 'node',
13+
});
14+
```
15+
16+
当你通过 [projects](/config/test/projects) 定义多个测试项目时,建议为每个项目指定唯一的名称,方便通过 [--project](/guide/basic/test-filter#根据项目名称过滤) 选项来过滤运行特定项目。
17+
18+
如果 project 未提供名称,Rstest 将会使用当前项目的 `package.json` 中定义的 `name` 属性,如果不存在,则使用文件夹名称。

0 commit comments

Comments
 (0)