Skip to content

Commit

Permalink
Unit Test Support example
Browse files Browse the repository at this point in the history
  • Loading branch information
ScriptedAlchemy committed May 19, 2024
1 parent 68fa25c commit b65bbf4
Show file tree
Hide file tree
Showing 35 changed files with 655 additions and 0 deletions.
23 changes: 23 additions & 0 deletions unit-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
21 changes: 21 additions & 0 deletions unit-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Rsbuild / Create React App Example

This example demos a basic host application loading remote component.

- `host` is the host application (unit_test-based).
- `remote` standalone application (unit_test-based) which exposes `Button` component.

# Running Demo

Run `pnpm run start`. This will build and serve both `host` and `remote` on ports 3001 and 3002 respectively.

- [localhost:3001](http://localhost:3000/) (HOST)
- [localhost:3002](http://localhost:3002/) (STANDALONE REMOTE)

# Running Cypress E2E Tests

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)

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.

["Best Practices, Rules amd more interesting information here](../../cypress/README.md)
4 changes: 4 additions & 0 deletions unit-test/cypress.env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"allure": true,
"allureResultsPath": "../cypress-e2e/results/allure-results"
}
45 changes: 45 additions & 0 deletions unit-test/e2e/checkCraApps.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { baseSelectors } from './../../cypress-e2e/common/selectors';
import { BaseMethods } from '../../cypress-e2e/common/base';
import { Constants } from '../../cypress-e2e/fixtures/constants';

const basePage: BaseMethods = new BaseMethods();

const appsData = [
{
appNameText: Constants.commonConstantsData.basicComponents.host,
host: 3000,
},
{
appNameText: Constants.commonConstantsData.basicComponents.remote,
host: 3002,
},
];

appsData.forEach((property: { appNameText: string; host: number }) => {
const appName = property.host === 3000 ? appsData[0].appNameText : appsData[1].appNameText;

describe('CRA', () => {
context(`Check ${appName}`, () => {
beforeEach(() => {
basePage.openLocalhost({
number: property.host,
});
});

it(`Check ${appName} elements exist on the page`, () => {
basePage.checkElementWithTextPresence({
selector: baseSelectors.tags.headers.h1,
text: Constants.commonConstantsData.basicComponents.basicHostRemote,
});
basePage.checkElementWithTextPresence({
selector: baseSelectors.tags.headers.h2,
text: property.appNameText,
});
basePage.checkElementWithTextPresence({
selector: baseSelectors.tags.coreElements.button,
text: Constants.elementsText.craApp.buttonText,
});
});
});
});
});
3 changes: 3 additions & 0 deletions unit-test/host/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
23 changes: 23 additions & 0 deletions unit-test/host/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
27 changes: 27 additions & 0 deletions unit-test/host/__tests__/app.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import App from '@src/App.js';

describe('App Component', () => {
beforeAll(async ()=>{
await require('federation-test')
});
test('renders the main heading', () => {
render(<App />);
const mainHeading = screen.getByTestId('main-heading');
expect(mainHeading).toBeInTheDocument();
});

test('renders the subheading', () => {
render(<App />);
const subHeading = screen.getByTestId('sub-heading');
expect(subHeading).toBeInTheDocument();
});

test('renders the RemoteButton with fallback', async () => {
render(<App />);
const remoteButton = await screen.findByTestId('remote-button');
expect(remoteButton).toBeInTheDocument();
});
});
10 changes: 10 additions & 0 deletions unit-test/host/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
moduleNameMapper: {
'^@src/(.*)$': '<rootDir>/src/$1'
},
testEnvironment: 'jsdom',
transform: {
'^.+\\.jsx?$': 'babel-jest'
},
setupFiles: ['<rootDir>/jest.setup.js']
};
153 changes: 153 additions & 0 deletions unit-test/host/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
const fs = require('fs');
const path = require('path');
const mfConfig = require('./modulefederation.config');
const fetch = require('node-fetch');
const {init, loadRemote} = require('@module-federation/runtime')


const sharedConfig = {};
for (const [packageName, packageConfig] of Object.entries(mfConfig.shared)) {
let version = false;
try {
version = require(path.join(packageName, 'package.json')).version;
} catch (e) {
// Handle error if needed
}
if (typeof packageConfig === 'string') {
sharedConfig[packageName] = {
version,
lib: () => require(packageName),
};
} else {
sharedConfig[packageName] = {
version,
...packageConfig,
lib: () => require(packageName),
};
}
}




module.exports = async () => {
let remotes = [];

const harnessPath = path.resolve(__dirname, 'node_modules', 'federation-test');

let harnessData = [];
for (const [remote, entry] of Object.entries(mfConfig.remotes)) {
const [name, url] = entry.split('@');
const manifest = url.replace('remoteEntry.js', 'mf-manifest.json');
const response = await fetch(manifest);
const data = await response.json();

const parsedPath = new URL(url).origin;
const subPath = data.metaData.remoteEntry.path;

const buildUrl = (parsedPath, subPath, file) => {
return subPath ? `${parsedPath}/${subPath}/${file}` : `${parsedPath}/${file}`;
};

remotes.push(buildUrl(parsedPath, subPath, data.metaData.remoteEntry.name));

const jsFiles = [
...data.shared.flatMap(shared => [...shared.assets.js.sync, ...shared.assets.js.async].map(file => buildUrl(parsedPath, subPath, file))),
...data.exposes.flatMap(expose => [...expose.assets.js.sync, ...expose.assets.js.async].map(file => buildUrl(parsedPath, subPath, file)))
];

const cssFiles = [
...data.shared.flatMap(shared => [...shared.assets.css.sync, ...shared.assets.css.async].map(file => buildUrl(parsedPath, subPath, file))),
...data.exposes.flatMap(expose => [...expose.assets.css.sync, ...expose.assets.css.async].map(file => buildUrl(parsedPath, subPath, file)))
];

remotes.push(...jsFiles, ...cssFiles);

const fakePackagePath = path.resolve(__dirname, 'node_modules', data.id);
const fakePackageJsonPath = path.join(fakePackagePath, 'package.json');
const fakePackageIndexPath = path.join(fakePackagePath, 'index.js');

if (!fs.existsSync(fakePackagePath)) {
fs.mkdirSync(fakePackagePath, { recursive: true });
}

const exportsContent = data.exposes.reduce((exportsObj, expose) => {
let exposeName = expose.name;
if (!exposeName.endsWith('.js')) {
exposeName += '.js';
}
exportsObj[expose.path] = './virtual' + exposeName;
const resolvePath = path.join(fakePackagePath, './virtual' + exposeName);

harnessData.push(resolvePath);

fs.writeFileSync(resolvePath, `
const container = require('./remoteEntry.js')[${JSON.stringify(data.id)}];
const target = {};
let e;
const cx = container.get(${JSON.stringify(expose.path)}).then((m) => {
e = m();
Object.assign(target, e);
});
module.exports = new Proxy(target, {
get(target, prop) {
if(prop === 'setupTest') return cx;
if (!e) {
return cx;
} else if (prop in e) {
return e[prop];
} else {
return e;
}
}
});
`, 'utf-8');
return exportsObj;
}, {});

const packageJsonContent = {
name: data.id,
version: '1.0.0',
// main: 'index.js',
exports: exportsContent
};
const indexJsContent = `
module.exports = () => 'Hello from fake package!';
`;

fs.writeFileSync(fakePackageJsonPath, JSON.stringify(packageJsonContent, null, 2));
fs.writeFileSync(fakePackageIndexPath, indexJsContent);

for (const fileUrl of remotes) {
const fileName = path.basename(fileUrl);
const filePath = path.join(fakePackagePath, fileName);
const fileResponse = await fetch(fileUrl);
const fileData = await fileResponse.buffer();
fs.writeFileSync(filePath, fileData);
}
}

if (!fs.existsSync(harnessPath)) {
fs.mkdirSync(harnessPath, { recursive: true });
}

fs.writeFileSync('node_modules/federation-test/index.js', `module.exports = Promise.all(${JSON.stringify(harnessData)}.map((p) => require(p).setupTest))`, 'utf-8');
fs.writeFileSync('node_modules/federation-test/package.json', '{"name": "federation-test", "main": "./index.js"}', 'utf-8');

// init({
// name: 'host',
// remotes: [{
// name: "remote",
// entry: "remote/remoteEntry.js",
// alias: "remote",
// type: 'commonjs'
// }],
// shared: sharedConfig,
// });

// await loadRemote('remote/Button').then(console.log)
};

21 changes: 21 additions & 0 deletions unit-test/host/modulefederation.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { dependencies } = require('./package.json');

module.exports = {
name: 'host',
library: {type: 'commonjs-module', name: 'host'},
remoteType: 'script',
remotes: {
remote: 'remote@http://localhost:3002/remoteEntry.js',
},
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies['react'],
},
'react-dom': {
singleton: true,
requiredVersion: dependencies['react-dom'],
},
},
};
44 changes: 44 additions & 0 deletions unit-test/host/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "unit_test_host",
"version": "0.0.0",
"dependencies": {
"deasync": "^0.1.29",
"fibers": "^5.0.3",
"future": "^2.3.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"sync-promise": "^1.1.0"
},
"scripts": {
"start": "rsbuild dev",
"build": "rsbuild build",
"preview": "rsbuild preview",
"test": "jest"
},
"eslintConfig": {},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/preset-env": "^7.24.5",
"@babel/preset-react": "^7.24.1",
"@module-federation/enhanced": "^0.1.13",
"@module-federation/runtime": "^0.1.13",
"@rsbuild/core": "0.6.15",
"@rsbuild/plugin-react": "0.6.15",
"@rspack/core": "0.6.5",
"babel-jest": "^29.7.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"node-fetch": "2.6.9"
}
}
Binary file added unit-test/host/public/favicon.ico
Binary file not shown.
13 changes: 13 additions & 0 deletions unit-test/host/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Host</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
Loading

0 comments on commit b65bbf4

Please sign in to comment.