feat: e2e commands (#3042)

This commit is contained in:
chenos 2023-11-16 12:33:56 +08:00 committed by GitHub
parent 14a21882e8
commit 03062a2b05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 233 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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);

View 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);
});
};

View File

@ -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);

View File

@ -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, {

View File

@ -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',