feat: e2e commands (#3042)
This commit is contained in:
parent
14a21882e8
commit
03062a2b05
@ -16,9 +16,6 @@ APP_KEY=test-key-e2e
|
|||||||
SOCKET_PATH=storage/gateway-e2e.sock
|
SOCKET_PATH=storage/gateway-e2e.sock
|
||||||
__E2E__=true
|
__E2E__=true
|
||||||
|
|
||||||
# 指定运行测试的服务地址;当 APP_BASE_URL 不为空时,将不会在本地启动测试服务
|
|
||||||
# APP_BASE_URL=http://localhost:20000
|
|
||||||
|
|
||||||
# 启用 mock-collections 插件
|
# 启用 mock-collections 插件
|
||||||
APPEND_PRESET_BUILT_IN_PLUGINS=mock-collections
|
APPEND_PRESET_BUILT_IN_PLUGINS=mock-collections
|
||||||
|
|
||||||
|
3
.github/workflows/nocobase-test-e2e.yml
vendored
3
.github/workflows/nocobase-test-e2e.yml
vendored
@ -62,9 +62,10 @@ jobs:
|
|||||||
__E2E__: true
|
__E2E__: true
|
||||||
- run: npx playwright install --with-deps
|
- run: npx playwright install --with-deps
|
||||||
- name: Test with postgres
|
- name: Test with postgres
|
||||||
run: yarn test:e2e
|
run: yarn e2e test -x --skip-reporter
|
||||||
env:
|
env:
|
||||||
__E2E__: true
|
__E2E__: true
|
||||||
|
APP_ENV: production
|
||||||
LOGGER_LEVEL: error
|
LOGGER_LEVEL: error
|
||||||
DB_DIALECT: postgres
|
DB_DIALECT: postgres
|
||||||
DB_HOST: postgres
|
DB_HOST: postgres
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"tar": "nocobase tar",
|
"tar": "nocobase tar",
|
||||||
"test": "nocobase test",
|
"test": "nocobase test",
|
||||||
"test:client": "vitest",
|
"test:client": "vitest",
|
||||||
|
"e2e": "nocobase e2e",
|
||||||
"test:e2e": "tsx ./scripts/runE2e.setup.ts",
|
"test:e2e": "tsx ./scripts/runE2e.setup.ts",
|
||||||
"test:e2e:codegen": "tsx ./scripts/codegen.setup.ts",
|
"test:e2e:codegen": "tsx ./scripts/codegen.setup.ts",
|
||||||
"test:e2e:server": "tsx ./scripts/nocobase.setup.ts",
|
"test:e2e:server": "tsx ./scripts/nocobase.setup.ts",
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
const { resolve } = require('path');
|
const { resolve } = require('path');
|
||||||
const { existsSync } = require('fs');
|
const fs = require('fs');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { genTsConfigPaths } = require('../src/util');
|
const { genTsConfigPaths } = require('../src/util');
|
||||||
|
|
||||||
@ -25,11 +25,26 @@ const env = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!process.env.APP_ENV_PATH && process.argv[2] && process.argv[2] === 'test') {
|
if (!process.env.APP_ENV_PATH && process.argv[2] && process.argv[2] === 'test') {
|
||||||
if (existsSync(resolve(process.cwd(), '.env.test'))) {
|
if (fs.existsSync(resolve(process.cwd(), '.env.test'))) {
|
||||||
process.env.APP_ENV_PATH = '.env.test';
|
process.env.APP_ENV_PATH = '.env.test';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.argv[2] === 'e2e') {
|
||||||
|
// 用于存放 playwright 自动生成的相关的文件
|
||||||
|
if (!fs.existsSync('playwright')) {
|
||||||
|
fs.mkdirSync('playwright');
|
||||||
|
}
|
||||||
|
if (!fs.existsSync('.env.e2e') && fs.existsSync('.env.e2e.example')) {
|
||||||
|
const env = fs.readFileSync('.env.e2e.example');
|
||||||
|
fs.writeFileSync('.env.e2e', env);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync('.env.e2e')) {
|
||||||
|
throw new Error('Please create .env.e2e file first!');
|
||||||
|
}
|
||||||
|
process.env.APP_ENV_PATH = '.env.e2e';
|
||||||
|
}
|
||||||
|
|
||||||
genTsConfigPaths();
|
genTsConfigPaths();
|
||||||
|
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
@ -42,6 +57,10 @@ for (const key in env) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.argv[2] === 'e2e' && !process.env.APP_BASE_URL) {
|
||||||
|
process.env.APP_BASE_URL = `http://127.0.0.1:${process.env.APP_PORT}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (require('semver').satisfies(process.version, '<16')) {
|
if (require('semver').satisfies(process.version, '<16')) {
|
||||||
console.error(chalk.red('[nocobase cli]: Node.js version must be >= 16'));
|
console.error(chalk.red('[nocobase cli]: Node.js version must be >= 16'));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
199
packages/core/cli/src/commands/e2e.js
Normal file
199
packages/core/cli/src/commands/e2e.js
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
const { Command } = require('commander');
|
||||||
|
const { run, isPortReachable } = require('../util');
|
||||||
|
const { execSync } = require('node:child_process');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查服务是否启动成功
|
||||||
|
*/
|
||||||
|
const checkServer = async (duration = 1000, max = 60 * 10) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let count = 0;
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
if (count++ > max) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return reject(new Error('Server start timeout.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (!(await checkPort(PORT))) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const url = `${process.env.APP_BASE_URL}/api/__health_check`;
|
||||||
|
// console.log('url', url);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(url)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
clearInterval(timer);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Request error:', error.message);
|
||||||
|
});
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 UI 是否启动成功
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
const checkUI = async (duration = 1000, max = 60 * 10) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let count = 0;
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
if (count++ > max) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return reject(new Error('UI start timeout.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(`${process.env.APP_BASE_URL}/__umi/api/bundle-status`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data === 'ok') {
|
||||||
|
clearInterval(timer);
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.data.bundleStatus.done) {
|
||||||
|
clearInterval(timer);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Request error:', error.message);
|
||||||
|
});
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function appReady() {
|
||||||
|
console.log('check server...');
|
||||||
|
await checkServer();
|
||||||
|
console.log('server is ready, check UI...');
|
||||||
|
await checkUI();
|
||||||
|
console.log('UI is ready.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runApp(options = {}) {
|
||||||
|
console.log('installing...');
|
||||||
|
await run('nocobase', ['install', '-f']);
|
||||||
|
if (await isPortReachable(process.env.APP_PORT)) {
|
||||||
|
console.log('app started');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('starting...');
|
||||||
|
run('nocobase', [process.env.APP_ENV === 'production' ? 'start' : 'dev'], options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonConfig = {
|
||||||
|
stdio: 'inherit',
|
||||||
|
};
|
||||||
|
|
||||||
|
const runCodegenSync = () => {
|
||||||
|
try {
|
||||||
|
execSync(
|
||||||
|
`npx playwright codegen --load-storage=playwright/.auth/codegen.auth.json ${process.env.APP_BASE_URL} --save-storage=playwright/.auth/codegen.auth.json`,
|
||||||
|
commonConfig,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message.includes('auth.json')) {
|
||||||
|
execSync(
|
||||||
|
`npx playwright codegen ${process.env.APP_BASE_URL} --save-storage=playwright/.auth/codegen.auth.json`,
|
||||||
|
commonConfig,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterArgv = () => {
|
||||||
|
const arr = process.argv.slice(4);
|
||||||
|
const argv = [];
|
||||||
|
for (let index = 0; index < arr.length; index++) {
|
||||||
|
const element = arr[index];
|
||||||
|
if (element === '--url') {
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (element.startsWith('--url=')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (element === '--skip-reporter') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
argv.push(element);
|
||||||
|
}
|
||||||
|
return argv;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Command} cli
|
||||||
|
*/
|
||||||
|
module.exports = (cli) => {
|
||||||
|
const e2e = cli.command('e2e').hook('preAction', () => {
|
||||||
|
if (process.env.APP_BASE_URL) {
|
||||||
|
process.env.APP_BASE_URL = process.env.APP_BASE_URL.replace('localhost', '127.0.0.1');
|
||||||
|
console.log('APP_BASE_URL:', process.env.APP_BASE_URL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
e2e
|
||||||
|
.command('test')
|
||||||
|
.allowUnknownOption()
|
||||||
|
.option('--url [url]')
|
||||||
|
.option('--skip-reporter')
|
||||||
|
.action(async (options) => {
|
||||||
|
if (options.skipReporter) {
|
||||||
|
process.env.PLAYWRIGHT_SKIP_REPORTER = true;
|
||||||
|
}
|
||||||
|
if (options.url) {
|
||||||
|
process.env.APP_BASE_URL = options.url.replace('localhost', '127.0.0.1');
|
||||||
|
} else {
|
||||||
|
await runApp({
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await appReady();
|
||||||
|
await run('npx', ['playwright', 'test', ...filterArgv()]);
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e
|
||||||
|
.command('codegen')
|
||||||
|
.allowUnknownOption()
|
||||||
|
.option('--url [url]')
|
||||||
|
.action(async (options) => {
|
||||||
|
if (options.url) {
|
||||||
|
process.env.APP_BASE_URL = options.url.replace('localhost', '127.0.0.1');
|
||||||
|
} else {
|
||||||
|
await runApp({
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await appReady();
|
||||||
|
runCodegenSync();
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e
|
||||||
|
.command('start-app')
|
||||||
|
.option('--production')
|
||||||
|
.option('--port [port]')
|
||||||
|
.action(async (options) => {
|
||||||
|
if (options.production) {
|
||||||
|
process.env.APP_ENV = 'production';
|
||||||
|
}
|
||||||
|
if (options.port) {
|
||||||
|
process.env.APP_PORT = options.port;
|
||||||
|
}
|
||||||
|
runApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e.command('reinstall-app').action(async (options) => {
|
||||||
|
await run('nocobase', ['install', '-f'], options);
|
||||||
|
});
|
||||||
|
};
|
@ -13,6 +13,7 @@ module.exports = (cli) => {
|
|||||||
require('./dev')(cli);
|
require('./dev')(cli);
|
||||||
require('./start')(cli);
|
require('./start')(cli);
|
||||||
require('./test')(cli);
|
require('./test')(cli);
|
||||||
|
require('./e2e')(cli);
|
||||||
require('./clean')(cli);
|
require('./clean')(cli);
|
||||||
require('./doc')(cli);
|
require('./doc')(cli);
|
||||||
require('./umi')(cli);
|
require('./umi')(cli);
|
||||||
|
@ -145,6 +145,12 @@ export class Gateway extends EventEmitter {
|
|||||||
async requestHandler(req: IncomingMessage, res: ServerResponse) {
|
async requestHandler(req: IncomingMessage, res: ServerResponse) {
|
||||||
const { pathname } = parse(req.url);
|
const { pathname } = parse(req.url);
|
||||||
|
|
||||||
|
if (pathname === '/__umi/api/bundle-status') {
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.end('ok');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/storage/uploads/')) {
|
if (pathname.startsWith('/storage/uploads/')) {
|
||||||
await compress(req, res);
|
await compress(req, res);
|
||||||
return handler(req, res, {
|
return handler(req, res, {
|
||||||
|
@ -22,7 +22,9 @@ export default defineConfig({
|
|||||||
workers: 1,
|
workers: 1,
|
||||||
|
|
||||||
// Reporter to use
|
// Reporter to use
|
||||||
reporter: [['html', { outputFolder: './playwright/tests-report' }]],
|
reporter: process.env.PLAYWRIGHT_SKIP_REPORTER
|
||||||
|
? undefined
|
||||||
|
: [['html', { outputFolder: './playwright/tests-report' }]],
|
||||||
|
|
||||||
outputDir: './playwright/test-results',
|
outputDir: './playwright/test-results',
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user