Skip to content

Commit 1c05552

Browse files
authored
test(e2e): add react-component example test (#239)
1 parent 6d92cf4 commit 1c05552

File tree

13 files changed

+244
-9
lines changed

13 files changed

+244
-9
lines changed

.github/workflows/test-ubuntu.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,12 @@ jobs:
103103
if: steps.changes.outputs.changed == 'true'
104104
run: pnpm run test:artifact
105105

106-
- name: E2E Test (Playwright)
107-
if: steps.changes.outputs.changed == 'true'
108-
run: pnpm run test:e2e
109-
110106
- name: Examples Test
111107
if: steps.changes.outputs.changed == 'true'
112108
run: |
113109
pnpm run build:examples
110+
111+
- name: E2E Test (Playwright)
112+
if: steps.changes.outputs.changed == 'true'
113+
run: pnpm run test:e2e
114+

.github/workflows/test-windows.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,10 @@ jobs:
112112
if: steps.changes.outputs.changed == 'true'
113113
run: pnpm run test:artifact
114114

115-
- name: E2E Test (Playwright)
116-
if: steps.changes.outputs.changed == 'true'
117-
run: pnpm run test:e2e
118-
119115
- name: Examples Test
120116
if: steps.changes.outputs.changed == 'true'
121117
run: pnpm run build:examples
118+
119+
- name: E2E Test (Playwright)
120+
if: steps.changes.outputs.changed == 'true'
121+
run: pnpm run test:e2e
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { dev } from '@e2e/helper/rsbuild';
2+
import { expect, test } from '@playwright/test';
3+
4+
test('should render example "react-component" successfully', async ({
5+
page,
6+
}) => {
7+
const rsbuild = await dev({
8+
cwd: __dirname,
9+
page,
10+
});
11+
12+
const h2El = page.locator('h2');
13+
await expect(h2El).toHaveText('Counter: 0');
14+
15+
const buttonEl = page.locator('#root button');
16+
17+
const [subtractEl, addEl] = await buttonEl.all();
18+
19+
await expect(h2El).toHaveText('Counter: 0');
20+
addEl?.click();
21+
await expect(h2El).toHaveText('Counter: 1');
22+
subtractEl?.click();
23+
await expect(h2El).toHaveText('Counter: 0');
24+
25+
await rsbuild.close();
26+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "react-component-e2e",
3+
"version": "1.0.0",
4+
"private": true
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineConfig } from '@rsbuild/core';
2+
import { pluginReact } from '@rsbuild/plugin-react';
3+
4+
export default defineConfig({
5+
plugins: [pluginReact()],
6+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Counter } from '@examples/react-component';
2+
3+
const App = () => (
4+
<div>
5+
<Counter />
6+
</div>
7+
);
8+
9+
export default App;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom/client';
3+
import App from './App';
4+
5+
const root = ReactDOM.createRoot(document.getElementById('root')!);
6+
root.render(
7+
<React.StrictMode>
8+
<App />
9+
</React.StrictMode>,
10+
);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"lib": ["DOM", "ES2020"],
5+
"module": "ESNext",
6+
"jsx": "react-jsx",
7+
"strict": true,
8+
"skipLibCheck": true,
9+
"isolatedModules": true,
10+
"resolveJsonModule": true,
11+
"moduleResolution": "bundler",
12+
"useDefineForClassFields": true
13+
},
14+
"include": ["src"]
15+
}

e2e/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66
"test": "playwright test --pass-with-no-tests"
77
},
88
"dependencies": {
9-
"react": "^18.3.1"
9+
"@examples/react-component": "workspace:*",
10+
"react": "^18.3.1",
11+
"react-dom": "^18.3.1"
1012
},
1113
"devDependencies": {
1214
"@e2e/helper": "workspace:*",
1315
"@playwright/test": "1.47.2",
1416
"@rsbuild/core": "1.0.7",
17+
"@rsbuild/plugin-react": "1.0.2",
1518
"@rslib/core": "workspace:*",
1619
"@rslib/tsconfig": "workspace:*",
1720
"@types/fs-extra": "^11.0.4",
1821
"@types/node": "~18.19.39",
1922
"@types/react": "^18.3.9",
23+
"@types/react-dom": "^18.3.0",
2024
"fast-glob": "^3.3.2",
2125
"fs-extra": "^11.2.0",
2226
"path-serializer": "0.0.6",

e2e/playwright.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ import { defineConfig } from '@playwright/test';
33
export default defineConfig({
44
// Playwright test files with `.pw.` to distinguish from Vitest test files
55
testMatch: /.*pw.(test|spec).(js|ts|mjs)/,
6+
// Retry on CI
7+
retries: process.env.CI ? 3 : 0,
8+
// Print line for each test being run in CI
9+
reporter: 'list',
610
});

e2e/scripts/rsbuild.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* The following code is modified based on
3+
* https://github.com/web-infra-dev/rsbuild/blob/c21e2130a285177b890fca543f70377b66d1ad73/e2e/scripts/shared.ts
4+
*/
5+
import net from 'node:net';
6+
import type { Page } from '@playwright/test';
7+
import type {
8+
CreateRsbuildOptions,
9+
RsbuildConfig,
10+
RsbuildPlugins,
11+
} from '@rsbuild/core';
12+
13+
const getHrefByEntryName = (entryName: string, port: number) => {
14+
const htmlRoot = new URL(`http://localhost:${port}`);
15+
const homeUrl = new URL(`${entryName}.html`, htmlRoot);
16+
17+
return homeUrl.href;
18+
};
19+
20+
const gotoPage = async (
21+
page: Page,
22+
rsbuild: { port: number },
23+
path = 'index',
24+
) => {
25+
const url = getHrefByEntryName(path, rsbuild.port);
26+
return page.goto(url);
27+
};
28+
29+
function isPortAvailable(port: number) {
30+
try {
31+
const server = net.createServer().listen(port);
32+
return new Promise((resolve) => {
33+
server.on('listening', () => {
34+
server.close();
35+
resolve(true);
36+
});
37+
38+
server.on('error', () => {
39+
resolve(false);
40+
});
41+
});
42+
} catch (err) {
43+
return false;
44+
}
45+
}
46+
47+
const portMap = new Map();
48+
// Available port ranges: 1024 ~ 65535
49+
// `10080` is not available in macOS CI, `> 50000` get 'permission denied' in Windows.
50+
// so we use `15000` ~ `45000`.
51+
async function getRandomPort(
52+
defaultPort = Math.ceil(Math.random() * 30000) + 15000,
53+
) {
54+
let port = defaultPort;
55+
while (true) {
56+
if (!portMap.get(port) && (await isPortAvailable(port))) {
57+
portMap.set(port, 1);
58+
return port;
59+
}
60+
port++;
61+
}
62+
}
63+
64+
const updateConfigForTest = async (
65+
originalConfig: RsbuildConfig,
66+
cwd: string = process.cwd(),
67+
) => {
68+
const { loadConfig, mergeRsbuildConfig } = await import('@rsbuild/core');
69+
const { content: loadedConfig } = await loadConfig({
70+
cwd,
71+
});
72+
73+
const baseConfig: RsbuildConfig = {
74+
dev: {
75+
progressBar: false,
76+
},
77+
server: {
78+
// make port random to avoid conflict
79+
port: await getRandomPort(),
80+
printUrls: false,
81+
},
82+
performance: {
83+
buildCache: false,
84+
printFileSize: false,
85+
},
86+
};
87+
88+
return mergeRsbuildConfig(baseConfig, loadedConfig, originalConfig);
89+
};
90+
91+
const createRsbuild = async (
92+
rsbuildOptions: CreateRsbuildOptions,
93+
plugins: RsbuildPlugins = [],
94+
) => {
95+
const { createRsbuild: createRsbuildInner } = await import('@rsbuild/core');
96+
97+
rsbuildOptions.rsbuildConfig ||= {};
98+
rsbuildOptions.rsbuildConfig.plugins = [
99+
...(rsbuildOptions.rsbuildConfig.plugins || []),
100+
...(plugins || []),
101+
];
102+
103+
const rsbuild = await createRsbuildInner(rsbuildOptions);
104+
return rsbuild;
105+
};
106+
107+
export async function dev({
108+
plugins,
109+
page,
110+
...options
111+
}: CreateRsbuildOptions & {
112+
plugins?: RsbuildPlugins;
113+
/**
114+
* Playwright Page instance.
115+
* This method will automatically goto the page.
116+
*/
117+
page?: Page;
118+
}) {
119+
process.env.NODE_ENV = 'development';
120+
121+
options.rsbuildConfig = await updateConfigForTest(
122+
options.rsbuildConfig || {},
123+
options.cwd,
124+
);
125+
126+
const rsbuild = await createRsbuild(options, plugins);
127+
const result = await rsbuild.startDevServer();
128+
129+
if (page) {
130+
await gotoPage(page, result);
131+
}
132+
133+
return {
134+
...result,
135+
instance: rsbuild,
136+
close: async () => result.server.close(),
137+
};
138+
}

examples/react-component/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"name": "@examples/react-component",
33
"private": true,
4+
"main": "./dist/cjs/index.js",
5+
"module": "./dist/esm/index.mjs",
6+
"types": "./dist/cjs/index.d.ts",
47
"scripts": {
58
"build": "rslib build"
69
},

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)