Skip to content

Commit b65bbf4

Browse files
Unit Test Support example
1 parent 68fa25c commit b65bbf4

35 files changed

+655
-0
lines changed

unit-test/.gitignore

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*

unit-test/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Rsbuild / Create React App Example
2+
3+
This example demos a basic host application loading remote component.
4+
5+
- `host` is the host application (unit_test-based).
6+
- `remote` standalone application (unit_test-based) which exposes `Button` component.
7+
8+
# Running Demo
9+
10+
Run `pnpm run start`. This will build and serve both `host` and `remote` on ports 3001 and 3002 respectively.
11+
12+
- [localhost:3001](http://localhost:3000/) (HOST)
13+
- [localhost:3002](http://localhost:3002/) (STANDALONE REMOTE)
14+
15+
# Running Cypress E2E Tests
16+
17+
To run tests in interactive mode, run `npm run cypress:debug` from the root directory of the project. It will open Cypress Test Runner and allow to run tests in interactive mode. [More info about "How to run tests"](../../cypress/README.md#how-to-run-tests)
18+
19+
To build app and run test in headless mode, run `yarn e2e:ci`. It will build app and run tests for this workspace in headless mode. If tets failed cypress will create `cypress` directory in sample root folder with screenshots and videos.
20+
21+
["Best Practices, Rules amd more interesting information here](../../cypress/README.md)

unit-test/cypress.env.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"allure": true,
3+
"allureResultsPath": "../cypress-e2e/results/allure-results"
4+
}

unit-test/e2e/checkCraApps.cy.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { baseSelectors } from './../../cypress-e2e/common/selectors';
2+
import { BaseMethods } from '../../cypress-e2e/common/base';
3+
import { Constants } from '../../cypress-e2e/fixtures/constants';
4+
5+
const basePage: BaseMethods = new BaseMethods();
6+
7+
const appsData = [
8+
{
9+
appNameText: Constants.commonConstantsData.basicComponents.host,
10+
host: 3000,
11+
},
12+
{
13+
appNameText: Constants.commonConstantsData.basicComponents.remote,
14+
host: 3002,
15+
},
16+
];
17+
18+
appsData.forEach((property: { appNameText: string; host: number }) => {
19+
const appName = property.host === 3000 ? appsData[0].appNameText : appsData[1].appNameText;
20+
21+
describe('CRA', () => {
22+
context(`Check ${appName}`, () => {
23+
beforeEach(() => {
24+
basePage.openLocalhost({
25+
number: property.host,
26+
});
27+
});
28+
29+
it(`Check ${appName} elements exist on the page`, () => {
30+
basePage.checkElementWithTextPresence({
31+
selector: baseSelectors.tags.headers.h1,
32+
text: Constants.commonConstantsData.basicComponents.basicHostRemote,
33+
});
34+
basePage.checkElementWithTextPresence({
35+
selector: baseSelectors.tags.headers.h2,
36+
text: property.appNameText,
37+
});
38+
basePage.checkElementWithTextPresence({
39+
selector: baseSelectors.tags.coreElements.button,
40+
text: Constants.elementsText.craApp.buttonText,
41+
});
42+
});
43+
});
44+
});
45+
});

unit-test/host/.babelrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["@babel/preset-env", "@babel/preset-react"]
3+
}

unit-test/host/.gitignore

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*

unit-test/host/__tests__/app.test.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import '@testing-library/jest-dom/extend-expect';
4+
import App from '@src/App.js';
5+
6+
describe('App Component', () => {
7+
beforeAll(async ()=>{
8+
await require('federation-test')
9+
});
10+
test('renders the main heading', () => {
11+
render(<App />);
12+
const mainHeading = screen.getByTestId('main-heading');
13+
expect(mainHeading).toBeInTheDocument();
14+
});
15+
16+
test('renders the subheading', () => {
17+
render(<App />);
18+
const subHeading = screen.getByTestId('sub-heading');
19+
expect(subHeading).toBeInTheDocument();
20+
});
21+
22+
test('renders the RemoteButton with fallback', async () => {
23+
render(<App />);
24+
const remoteButton = await screen.findByTestId('remote-button');
25+
expect(remoteButton).toBeInTheDocument();
26+
});
27+
});

unit-test/host/jest.config.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
moduleNameMapper: {
3+
'^@src/(.*)$': '<rootDir>/src/$1'
4+
},
5+
testEnvironment: 'jsdom',
6+
transform: {
7+
'^.+\\.jsx?$': 'babel-jest'
8+
},
9+
setupFiles: ['<rootDir>/jest.setup.js']
10+
};

unit-test/host/jest.setup.js

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const mfConfig = require('./modulefederation.config');
4+
const fetch = require('node-fetch');
5+
const {init, loadRemote} = require('@module-federation/runtime')
6+
7+
8+
const sharedConfig = {};
9+
for (const [packageName, packageConfig] of Object.entries(mfConfig.shared)) {
10+
let version = false;
11+
try {
12+
version = require(path.join(packageName, 'package.json')).version;
13+
} catch (e) {
14+
// Handle error if needed
15+
}
16+
if (typeof packageConfig === 'string') {
17+
sharedConfig[packageName] = {
18+
version,
19+
lib: () => require(packageName),
20+
};
21+
} else {
22+
sharedConfig[packageName] = {
23+
version,
24+
...packageConfig,
25+
lib: () => require(packageName),
26+
};
27+
}
28+
}
29+
30+
31+
32+
33+
module.exports = async () => {
34+
let remotes = [];
35+
36+
const harnessPath = path.resolve(__dirname, 'node_modules', 'federation-test');
37+
38+
let harnessData = [];
39+
for (const [remote, entry] of Object.entries(mfConfig.remotes)) {
40+
const [name, url] = entry.split('@');
41+
const manifest = url.replace('remoteEntry.js', 'mf-manifest.json');
42+
const response = await fetch(manifest);
43+
const data = await response.json();
44+
45+
const parsedPath = new URL(url).origin;
46+
const subPath = data.metaData.remoteEntry.path;
47+
48+
const buildUrl = (parsedPath, subPath, file) => {
49+
return subPath ? `${parsedPath}/${subPath}/${file}` : `${parsedPath}/${file}`;
50+
};
51+
52+
remotes.push(buildUrl(parsedPath, subPath, data.metaData.remoteEntry.name));
53+
54+
const jsFiles = [
55+
...data.shared.flatMap(shared => [...shared.assets.js.sync, ...shared.assets.js.async].map(file => buildUrl(parsedPath, subPath, file))),
56+
...data.exposes.flatMap(expose => [...expose.assets.js.sync, ...expose.assets.js.async].map(file => buildUrl(parsedPath, subPath, file)))
57+
];
58+
59+
const cssFiles = [
60+
...data.shared.flatMap(shared => [...shared.assets.css.sync, ...shared.assets.css.async].map(file => buildUrl(parsedPath, subPath, file))),
61+
...data.exposes.flatMap(expose => [...expose.assets.css.sync, ...expose.assets.css.async].map(file => buildUrl(parsedPath, subPath, file)))
62+
];
63+
64+
remotes.push(...jsFiles, ...cssFiles);
65+
66+
const fakePackagePath = path.resolve(__dirname, 'node_modules', data.id);
67+
const fakePackageJsonPath = path.join(fakePackagePath, 'package.json');
68+
const fakePackageIndexPath = path.join(fakePackagePath, 'index.js');
69+
70+
if (!fs.existsSync(fakePackagePath)) {
71+
fs.mkdirSync(fakePackagePath, { recursive: true });
72+
}
73+
74+
const exportsContent = data.exposes.reduce((exportsObj, expose) => {
75+
let exposeName = expose.name;
76+
if (!exposeName.endsWith('.js')) {
77+
exposeName += '.js';
78+
}
79+
exportsObj[expose.path] = './virtual' + exposeName;
80+
const resolvePath = path.join(fakePackagePath, './virtual' + exposeName);
81+
82+
harnessData.push(resolvePath);
83+
84+
fs.writeFileSync(resolvePath, `
85+
const container = require('./remoteEntry.js')[${JSON.stringify(data.id)}];
86+
const target = {};
87+
88+
let e;
89+
const cx = container.get(${JSON.stringify(expose.path)}).then((m) => {
90+
e = m();
91+
Object.assign(target, e);
92+
});
93+
94+
95+
module.exports = new Proxy(target, {
96+
get(target, prop) {
97+
if(prop === 'setupTest') return cx;
98+
if (!e) {
99+
return cx;
100+
} else if (prop in e) {
101+
return e[prop];
102+
} else {
103+
return e;
104+
}
105+
}
106+
});
107+
`, 'utf-8');
108+
return exportsObj;
109+
}, {});
110+
111+
const packageJsonContent = {
112+
name: data.id,
113+
version: '1.0.0',
114+
// main: 'index.js',
115+
exports: exportsContent
116+
};
117+
const indexJsContent = `
118+
module.exports = () => 'Hello from fake package!';
119+
`;
120+
121+
fs.writeFileSync(fakePackageJsonPath, JSON.stringify(packageJsonContent, null, 2));
122+
fs.writeFileSync(fakePackageIndexPath, indexJsContent);
123+
124+
for (const fileUrl of remotes) {
125+
const fileName = path.basename(fileUrl);
126+
const filePath = path.join(fakePackagePath, fileName);
127+
const fileResponse = await fetch(fileUrl);
128+
const fileData = await fileResponse.buffer();
129+
fs.writeFileSync(filePath, fileData);
130+
}
131+
}
132+
133+
if (!fs.existsSync(harnessPath)) {
134+
fs.mkdirSync(harnessPath, { recursive: true });
135+
}
136+
137+
fs.writeFileSync('node_modules/federation-test/index.js', `module.exports = Promise.all(${JSON.stringify(harnessData)}.map((p) => require(p).setupTest))`, 'utf-8');
138+
fs.writeFileSync('node_modules/federation-test/package.json', '{"name": "federation-test", "main": "./index.js"}', 'utf-8');
139+
140+
// init({
141+
// name: 'host',
142+
// remotes: [{
143+
// name: "remote",
144+
// entry: "remote/remoteEntry.js",
145+
// alias: "remote",
146+
// type: 'commonjs'
147+
// }],
148+
// shared: sharedConfig,
149+
// });
150+
151+
// await loadRemote('remote/Button').then(console.log)
152+
};
153+
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const { dependencies } = require('./package.json');
2+
3+
module.exports = {
4+
name: 'host',
5+
library: {type: 'commonjs-module', name: 'host'},
6+
remoteType: 'script',
7+
remotes: {
8+
remote: 'remote@http://localhost:3002/remoteEntry.js',
9+
},
10+
shared: {
11+
...dependencies,
12+
react: {
13+
singleton: true,
14+
requiredVersion: dependencies['react'],
15+
},
16+
'react-dom': {
17+
singleton: true,
18+
requiredVersion: dependencies['react-dom'],
19+
},
20+
},
21+
};

unit-test/host/package.json

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "unit_test_host",
3+
"version": "0.0.0",
4+
"dependencies": {
5+
"deasync": "^0.1.29",
6+
"fibers": "^5.0.3",
7+
"future": "^2.3.1",
8+
"react": "17.0.2",
9+
"react-dom": "17.0.2",
10+
"sync-promise": "^1.1.0"
11+
},
12+
"scripts": {
13+
"start": "rsbuild dev",
14+
"build": "rsbuild build",
15+
"preview": "rsbuild preview",
16+
"test": "jest"
17+
},
18+
"eslintConfig": {},
19+
"browserslist": {
20+
"production": [
21+
">0.2%",
22+
"not dead",
23+
"not op_mini all"
24+
],
25+
"development": [
26+
"last 1 chrome version",
27+
"last 1 firefox version",
28+
"last 1 safari version"
29+
]
30+
},
31+
"devDependencies": {
32+
"@babel/preset-env": "^7.24.5",
33+
"@babel/preset-react": "^7.24.1",
34+
"@module-federation/enhanced": "^0.1.13",
35+
"@module-federation/runtime": "^0.1.13",
36+
"@rsbuild/core": "0.6.15",
37+
"@rsbuild/plugin-react": "0.6.15",
38+
"@rspack/core": "0.6.5",
39+
"babel-jest": "^29.7.0",
40+
"jest": "^29.7.0",
41+
"jest-environment-jsdom": "^29.7.0",
42+
"node-fetch": "2.6.9"
43+
}
44+
}

unit-test/host/public/favicon.ico

3.78 KB
Binary file not shown.

unit-test/host/public/index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<title>Host</title>
8+
</head>
9+
<body>
10+
<noscript>You need to enable JavaScript to run this app.</noscript>
11+
<div id="root"></div>
12+
</body>
13+
</html>

0 commit comments

Comments
 (0)