Skip to content

Commit 753b566

Browse files
authored
feat(test): support inline Rstest projects (#48)
1 parent 62207ea commit 753b566

22 files changed

Lines changed: 473 additions & 20 deletions

File tree

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,54 @@ For example, import Rstest APIs without adding `@rstest/core` as a direct depend
113113
import { expect, test } from 'rstack/test';
114114
```
115115

116+
## Test projects
117+
118+
`define.test()` accepts an Rstest configuration. For a single project, Rstack automatically applies the `define.app()` configuration, or falls back to `define.lib()`:
119+
120+
```ts
121+
import { define } from 'rstack';
122+
123+
define.app({
124+
// Rsbuild configuration
125+
});
126+
127+
define.test({
128+
testEnvironment: 'happy-dom',
129+
});
130+
```
131+
132+
For multiple test environments in the same app or library, use inline projects:
133+
134+
```ts
135+
import { define } from 'rstack';
136+
import { defineInlineProject } from 'rstack/test';
137+
138+
define.app({
139+
// Shared by all inline projects
140+
});
141+
142+
define.test({
143+
projects: [
144+
defineInlineProject({
145+
name: 'node',
146+
include: ['./tests/node/**/*.test.ts'],
147+
testEnvironment: 'node',
148+
}),
149+
defineInlineProject({
150+
name: 'dom',
151+
include: ['./tests/dom/**/*.test.tsx'],
152+
testEnvironment: 'happy-dom',
153+
}),
154+
],
155+
});
156+
```
157+
158+
Rstack applies the shared app or library adapter to every inline project without an explicit `extends`. String project entries are passed to Rstest unchanged and load their own configuration.
159+
160+
Run all projects with `rs test`, or select one with `rs test --project dom`.
161+
162+
See [`examples/rstest-inline-projects`](./examples/rstest-inline-projects) for a complete React SSR example.
163+
116164
## Credits
117165

118166
Rstack CLI is inspired by:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
["index", "commands"]
1+
["index", "commands", "testing"]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Testing
2+
3+
Rstack uses [Rstest](https://rstest.rs/) for testing. The value passed to `define.test()` is an Rstest configuration, similar to using Rstest's `defineConfig()` directly.
4+
5+
Test APIs and configuration helpers are available from `rstack/test`:
6+
7+
```ts
8+
import { defineInlineProject, expect, test } from 'rstack/test';
9+
```
10+
11+
## Single project
12+
13+
For a single test project, pass the Rstest options directly:
14+
15+
```ts title="rstack.config.ts"
16+
import { define } from 'rstack';
17+
18+
define.app({
19+
// Rsbuild configuration
20+
});
21+
22+
define.test({
23+
testEnvironment: 'happy-dom',
24+
setupFiles: ['./tests/rstest.setup.ts'],
25+
});
26+
```
27+
28+
Unless `extends` is provided explicitly, Rstack automatically extends the test configuration from `define.app()`. If there is no app configuration, it falls back to `define.lib()`.
29+
30+
## Inline projects
31+
32+
Use Rstest inline projects when one app or library needs multiple test configurations, such as separate Node.js and DOM test environments:
33+
34+
```ts title="rstack.config.ts"
35+
import { define } from 'rstack';
36+
import { defineInlineProject } from 'rstack/test';
37+
38+
define.app({
39+
// Shared by both inline projects
40+
});
41+
42+
define.test({
43+
coverage: {
44+
enabled: true,
45+
},
46+
projects: [
47+
defineInlineProject({
48+
name: 'node',
49+
include: ['./tests/node/**/*.test.ts'],
50+
testEnvironment: 'node',
51+
}),
52+
defineInlineProject({
53+
name: 'dom',
54+
include: ['./tests/dom/**/*.test.tsx'],
55+
testEnvironment: 'happy-dom',
56+
setupFiles: ['./tests/rstest.setup.ts'],
57+
}),
58+
],
59+
});
60+
```
61+
62+
Rstack applies the shared `define.app()` or `define.lib()` adapter to every inline project that does not define its own `extends`. Function-based app or library configuration is resolved once and shared by all inline projects.
63+
64+
Run all projects:
65+
66+
```bash
67+
rs test
68+
```
69+
70+
Run one project by name:
71+
72+
```bash
73+
rs test --project dom
74+
```
75+
76+
See [`examples/rstest-inline-projects`](https://github.com/rstackjs/rstack-cli/tree/main/examples/rstest-inline-projects) for a complete example that tests React server rendering in Node.js and client rendering in happy-dom.
77+
78+
## Custom `extends`
79+
80+
An inline project can opt out of automatic app or library inheritance by providing `extends` explicitly:
81+
82+
```ts
83+
import { defineInlineProject } from 'rstack/test';
84+
85+
const customProject = defineInlineProject({
86+
name: 'custom',
87+
extends: customAdapter(),
88+
});
89+
```
90+
91+
If the root `define.test()` configuration provides `extends`, Rstack leaves the complete test configuration unchanged.
92+
93+
## External projects
94+
95+
String project entries are passed to Rstest unchanged:
96+
97+
```ts
98+
import { define } from 'rstack';
99+
100+
define.test({
101+
projects: ['./legacy/rstest.config.ts'],
102+
});
103+
```
104+
105+
External string projects load their own Rstest configuration and do not inherit the current `define.app()` or `define.lib()` configuration. Use inline projects when projects should share the current Rstack build configuration.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@examples/rstest-inline-projects",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"build": "rs build",
7+
"dev": "rs dev",
8+
"preview": "rs preview",
9+
"test": "rs test"
10+
},
11+
"dependencies": {
12+
"react": "catalog:",
13+
"react-dom": "catalog:"
14+
},
15+
"devDependencies": {
16+
"@rsbuild/plugin-react": "catalog:",
17+
"@testing-library/dom": "catalog:",
18+
"@testing-library/react": "catalog:",
19+
"@types/node": "catalog:",
20+
"@types/react": "catalog:",
21+
"@types/react-dom": "catalog:",
22+
"happy-dom": "catalog:",
23+
"rstack": "workspace:*",
24+
"typescript": "catalog:"
25+
}
26+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { define } from 'rstack';
2+
import { defineInlineProject } from 'rstack/test';
3+
4+
define.app(async () => {
5+
const { pluginReact } = await import('@rsbuild/plugin-react');
6+
7+
return {
8+
plugins: [pluginReact()],
9+
};
10+
});
11+
12+
define.test({
13+
projects: [
14+
defineInlineProject({
15+
name: 'ssr',
16+
include: ['./tests/ssr.test.tsx'],
17+
testEnvironment: 'node',
18+
}),
19+
defineInlineProject({
20+
name: 'dom',
21+
include: ['./tests/dom.test.tsx'],
22+
testEnvironment: 'happy-dom',
23+
}),
24+
],
25+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function App() {
2+
return (
3+
<main>
4+
<h1>Rstack React SSR</h1>
5+
<p>Rendered on the server and hydrated in the browser.</p>
6+
</main>
7+
);
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { hydrateRoot } from 'react-dom/client';
2+
import App from './App';
3+
4+
const root = document.getElementById('root');
5+
6+
if (root) {
7+
hydrateRoot(root, <App />);
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { expect, test } from 'rstack/test';
2+
import { render, screen } from '@testing-library/react';
3+
import App from '../src/App';
4+
5+
test('renders the app in a DOM environment', () => {
6+
render(<App />);
7+
8+
expect(screen.getByRole('heading', { name: 'Rstack React SSR' })).toBeTruthy();
9+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { expect, test } from 'rstack/test';
2+
import { renderToString } from 'react-dom/server';
3+
import App from '../src/App';
4+
5+
test('renders the app on the server', () => {
6+
const html = renderToString(<App />);
7+
8+
expect(html).toContain('Rstack React SSR');
9+
expect(html).toContain('Rendered on the server');
10+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["DOM", "ES2023"],
4+
"jsx": "react-jsx",
5+
"target": "ES2023",
6+
"noEmit": true,
7+
"skipLibCheck": true,
8+
"types": ["rstack/types", "node"],
9+
"moduleDetection": "force",
10+
"moduleResolution": "bundler",
11+
"verbatimModuleSyntax": true,
12+
"resolveJsonModule": true,
13+
"allowImportingTsExtensions": true,
14+
"strict": true
15+
},
16+
"include": ["src", "tests", "rstack.config.ts"]
17+
}

0 commit comments

Comments
 (0)