@epoint-testtech/stage-create 0.0.3-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +129 -0
- package/dist/render-template.d.ts +10 -0
- package/dist/render-template.js +34 -0
- package/dist/stage-context.d.ts +27 -0
- package/dist/stage-context.js +175 -0
- package/package.json +33 -0
- package/templates/default/.env.example +18 -0
- package/templates/default/package.json.tpl +20 -0
- package/templates/default/playwright.config.ts +44 -0
- package/templates/default/setup-mac.sh +29 -0
- package/templates/default/setup-windows.cmd +13 -0
- package/templates/default/setup-windows.ps1 +26 -0
- package/templates/default/src/global.setup.ts +5 -0
- package/templates/default/tsconfig.json +12 -0
- package/templates/glue/.env.example +18 -0
- package/templates/glue/package.json.tpl +21 -0
- package/templates/glue/playwright.config.ts +56 -0
- package/templates/glue/src/data/stage-config.yaml.tpl +13 -0
- package/templates/glue/src/gap-executor/gap-executor.ts +162 -0
- package/templates/glue/src/gap-executor/gap-healer.ts +50 -0
- package/templates/glue/src/gap-executor/page-structure-observer.ts +102 -0
- package/templates/glue/src/gap-executor/runtime-runner.ts +817 -0
- package/templates/glue/src/report/glue-report.ts +855 -0
- package/templates/glue/src/report/run-info.ts +85 -0
- package/templates/glue/src/skeletons/crud.skeleton.ts +450 -0
- package/templates/glue/src/skeletons/export.skeleton.ts +114 -0
- package/templates/glue/src/skeletons/index.ts +18 -0
- package/templates/glue/src/skeletons/login.skeleton.ts +104 -0
- package/templates/glue/src/skeletons/menu.skeleton.ts +37 -0
- package/templates/glue/src/tests/example.spec.ts +99 -0
- package/templates/glue/src/web/component/anchor-types.ts +9 -0
- package/templates/glue/src/web/component/dataexport-component.ts +143 -0
- package/templates/glue/src/web/component/index.ts +2 -0
- package/templates/glue/src/web/component/listbox-component.ts +41 -0
- package/templates/glue/tsconfig.json +12 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { program } from 'commander';
|
|
6
|
+
import prompts from 'prompts';
|
|
7
|
+
import { copyTemplateDir } from './render-template.js';
|
|
8
|
+
import { writeGlueProjectContext, validateRequiredContextInput } from './stage-context.js';
|
|
9
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
10
|
+
const TEMPLATES_DIR = path.resolve(path.dirname(currentFilePath), '..', 'templates');
|
|
11
|
+
const DEFAULT_STAGE_CORE_DEPENDENCY = '^0.0.3-alpha.1';
|
|
12
|
+
const DEFAULT_EP_STAGE_SKILL_DEPENDENCY = '^0.0.3-alpha.1';
|
|
13
|
+
program
|
|
14
|
+
.name('stage-create')
|
|
15
|
+
.description('Epoint Stage 项目脚手架')
|
|
16
|
+
.argument('[project-name]', '项目名称')
|
|
17
|
+
.option('--mode <mode>', '模式:default 或 glue')
|
|
18
|
+
.action(async (projectName, options) => {
|
|
19
|
+
const answers = await prompts([
|
|
20
|
+
{
|
|
21
|
+
type: projectName ? undefined : 'text',
|
|
22
|
+
name: 'projectName',
|
|
23
|
+
message: '项目名称',
|
|
24
|
+
initial: 'stage-project',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: options?.mode ? undefined : 'select',
|
|
28
|
+
name: 'mode',
|
|
29
|
+
message: '选择模式',
|
|
30
|
+
choices: [
|
|
31
|
+
{ title: '默认 — 基础 playwright.config.ts 配置', value: 'default' },
|
|
32
|
+
{ title: '胶水模式 — 完整骨架(CrudPage + LoginPage + MenuPage 示例)', value: 'glue' },
|
|
33
|
+
],
|
|
34
|
+
initial: 0,
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
const rawName = projectName ?? answers.projectName;
|
|
38
|
+
const mode = options?.mode ?? answers.mode ?? 'default';
|
|
39
|
+
if (!rawName) {
|
|
40
|
+
console.error('项目名称不能为空');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
// 支持传入路径:从路径中提取项目名(如 /tmp/my-project → my-project)
|
|
44
|
+
const name = path.basename(rawName);
|
|
45
|
+
const templateDir = path.join(TEMPLATES_DIR, mode);
|
|
46
|
+
const outputDir = path.resolve(rawName);
|
|
47
|
+
const stageCoreDependency = process.env.STAGE_CORE_DEPENDENCY ?? DEFAULT_STAGE_CORE_DEPENDENCY;
|
|
48
|
+
const epStageSkillDependency = process.env.EP_STAGE_SKILL_DEPENDENCY ?? DEFAULT_EP_STAGE_SKILL_DEPENDENCY;
|
|
49
|
+
if (fs.existsSync(outputDir)) {
|
|
50
|
+
console.error(`目录已存在: ${outputDir}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const glueContextAnswers = mode === 'glue'
|
|
54
|
+
? await readGlueContextAnswers()
|
|
55
|
+
: {};
|
|
56
|
+
// glue 模式必填项校验必须早于 mkdirSync,避免缺失输入时留下空项目骨架。
|
|
57
|
+
if (mode === 'glue') {
|
|
58
|
+
validateRequiredContextInput({
|
|
59
|
+
loginSystemUrl: glueContextAnswers.loginSystemUrl ?? '',
|
|
60
|
+
loginUsername: glueContextAnswers.loginUsername ?? '',
|
|
61
|
+
loginPassword: glueContextAnswers.loginPassword ?? '',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
console.log('生成中...');
|
|
65
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
66
|
+
copyTemplateDir(templateDir, outputDir, {
|
|
67
|
+
projectName: name,
|
|
68
|
+
stageCoreDependency,
|
|
69
|
+
epStageSkillDependency,
|
|
70
|
+
});
|
|
71
|
+
if (mode === 'glue') {
|
|
72
|
+
writeGlueProjectContext({
|
|
73
|
+
projectName: name,
|
|
74
|
+
projectDir: outputDir,
|
|
75
|
+
loginSystemUrl: glueContextAnswers.loginSystemUrl ?? '',
|
|
76
|
+
loginUsername: glueContextAnswers.loginUsername ?? '',
|
|
77
|
+
loginPassword: glueContextAnswers.loginPassword ?? '',
|
|
78
|
+
knowledgeRoot: glueContextAnswers.knowledgeRoot,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
console.log(`\n完成!执行以下命令开始:`);
|
|
82
|
+
console.log(` cd ${outputDir}`);
|
|
83
|
+
console.log(` pnpm install`);
|
|
84
|
+
if (mode === 'default') {
|
|
85
|
+
console.log(` cp .env.example .env`);
|
|
86
|
+
}
|
|
87
|
+
console.log(` pnpm test`);
|
|
88
|
+
});
|
|
89
|
+
program.parse();
|
|
90
|
+
/**
|
|
91
|
+
* 读取 glue 模式项目上下文输入。
|
|
92
|
+
*
|
|
93
|
+
* @returns 登录配置和可选知识库路径。
|
|
94
|
+
*/
|
|
95
|
+
async function readGlueContextAnswers() {
|
|
96
|
+
if (!process.stdin.isTTY) {
|
|
97
|
+
const [loginSystemUrl, loginUsername, loginPassword, knowledgeRoot] = fs
|
|
98
|
+
.readFileSync(0, 'utf8')
|
|
99
|
+
.split(/\r?\n/);
|
|
100
|
+
return {
|
|
101
|
+
loginSystemUrl,
|
|
102
|
+
loginUsername,
|
|
103
|
+
loginPassword,
|
|
104
|
+
knowledgeRoot,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return prompts([
|
|
108
|
+
{
|
|
109
|
+
type: 'text',
|
|
110
|
+
name: 'loginSystemUrl',
|
|
111
|
+
message: '登录页面地址',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
name: 'loginUsername',
|
|
116
|
+
message: '登录用户名',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'password',
|
|
120
|
+
name: 'loginPassword',
|
|
121
|
+
message: '登录密码',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
name: 'knowledgeRoot',
|
|
126
|
+
message: '知识库路径(可选)',
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用变量字典渲染模板字符串。
|
|
3
|
+
* 模板占位符格式:{{variableName}}
|
|
4
|
+
*/
|
|
5
|
+
export declare function renderTemplate(content: string, vars: Record<string, string>): string;
|
|
6
|
+
/**
|
|
7
|
+
* 递归复制模板目录,渲染所有 .tpl 文件后去掉 .tpl 后缀。
|
|
8
|
+
* 非模板文件直接复制。
|
|
9
|
+
*/
|
|
10
|
+
export declare function copyTemplateDir(templateDir: string, outputDir: string, vars: Record<string, string>): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* 用变量字典渲染模板字符串。
|
|
5
|
+
* 模板占位符格式:{{variableName}}
|
|
6
|
+
*/
|
|
7
|
+
export function renderTemplate(content, vars) {
|
|
8
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 递归复制模板目录,渲染所有 .tpl 文件后去掉 .tpl 后缀。
|
|
12
|
+
* 非模板文件直接复制。
|
|
13
|
+
*/
|
|
14
|
+
export function copyTemplateDir(templateDir, outputDir, vars) {
|
|
15
|
+
const entries = fs.readdirSync(templateDir, { withFileTypes: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const srcPath = path.join(templateDir, entry.name);
|
|
18
|
+
const destName = entry.name.endsWith('.tpl')
|
|
19
|
+
? entry.name.replace(/\.tpl$/, '')
|
|
20
|
+
: entry.name;
|
|
21
|
+
const destPath = path.join(outputDir, destName);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
24
|
+
copyTemplateDir(srcPath, destPath, vars);
|
|
25
|
+
}
|
|
26
|
+
else if (entry.name.endsWith('.tpl')) {
|
|
27
|
+
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
28
|
+
fs.writeFileSync(destPath, renderTemplate(content, vars));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
fs.copyFileSync(srcPath, destPath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type WriteGlueProjectContextInput = {
|
|
2
|
+
projectName: string;
|
|
3
|
+
projectDir: string;
|
|
4
|
+
loginSystemUrl: string;
|
|
5
|
+
loginUsername: string;
|
|
6
|
+
loginPassword: string;
|
|
7
|
+
knowledgeRoot?: string;
|
|
8
|
+
homeDir?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* 写入 glue 项目的运行上下文文件和用户级项目索引。
|
|
12
|
+
*
|
|
13
|
+
* @param input - 项目目录、登录配置和可选知识库路径。
|
|
14
|
+
* @returns 写入的 .env、stage-context.md 和项目索引路径。
|
|
15
|
+
*/
|
|
16
|
+
export declare function writeGlueProjectContext(input: WriteGlueProjectContextInput): {
|
|
17
|
+
envPath: string;
|
|
18
|
+
stageContextPath: string;
|
|
19
|
+
projectIndexPath: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 校验 glue 模式运行所需的必填输入。
|
|
23
|
+
*
|
|
24
|
+
* @param input - glue 项目上下文输入。
|
|
25
|
+
* @throws 当登录地址、用户名或密码为空时抛出错误。
|
|
26
|
+
*/
|
|
27
|
+
export declare function validateRequiredContextInput(input: Pick<WriteGlueProjectContextInput, 'loginSystemUrl' | 'loginUsername' | 'loginPassword'>): void;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import JSON5 from 'json5';
|
|
5
|
+
/**
|
|
6
|
+
* 写入 glue 项目的运行上下文文件和用户级项目索引。
|
|
7
|
+
*
|
|
8
|
+
* @param input - 项目目录、登录配置和可选知识库路径。
|
|
9
|
+
* @returns 写入的 .env、stage-context.md 和项目索引路径。
|
|
10
|
+
*/
|
|
11
|
+
export function writeGlueProjectContext(input) {
|
|
12
|
+
const projectDir = path.resolve(input.projectDir);
|
|
13
|
+
const envPath = path.join(projectDir, '.env');
|
|
14
|
+
const stageContextPath = path.join(projectDir, 'stage-context.md');
|
|
15
|
+
const projectIndexPath = path.join(input.homeDir ?? os.homedir(), '.ep-stage', 'projects.index.json5');
|
|
16
|
+
const knowledgeRoot = normalizeOptionalPath(input.knowledgeRoot);
|
|
17
|
+
const codeListPaths = getExistingCodeListPaths(knowledgeRoot);
|
|
18
|
+
validateRequiredContextInput(input);
|
|
19
|
+
fs.writeFileSync(envPath, renderEnv(input), 'utf8');
|
|
20
|
+
fs.writeFileSync(stageContextPath, renderStageContext({
|
|
21
|
+
projectName: input.projectName,
|
|
22
|
+
knowledgeRoot,
|
|
23
|
+
codeListPaths,
|
|
24
|
+
}), 'utf8');
|
|
25
|
+
const index = readProjectIndex(projectIndexPath);
|
|
26
|
+
const nextEntry = {
|
|
27
|
+
projectName: input.projectName,
|
|
28
|
+
projectDir,
|
|
29
|
+
mode: 'glue',
|
|
30
|
+
envPath: '.env',
|
|
31
|
+
...(knowledgeRoot ? { knowledgeRoot } : {}),
|
|
32
|
+
codeListPaths,
|
|
33
|
+
stageContextPath,
|
|
34
|
+
};
|
|
35
|
+
const dedupedProjects = index.projects.filter((project) => path.resolve(project.projectDir) !== projectDir);
|
|
36
|
+
fs.mkdirSync(path.dirname(projectIndexPath), { recursive: true });
|
|
37
|
+
fs.writeFileSync(projectIndexPath, `${JSON5.stringify({ version: 1, projects: [...dedupedProjects, nextEntry] }, null, 2)}\n`, 'utf8');
|
|
38
|
+
return { envPath, stageContextPath, projectIndexPath };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 渲染 glue 模式运行所需的 .env 文件。
|
|
42
|
+
*
|
|
43
|
+
* @param input - 登录地址、账号和密码。
|
|
44
|
+
* @returns .env 文件内容。
|
|
45
|
+
*/
|
|
46
|
+
function renderEnv(input) {
|
|
47
|
+
return [
|
|
48
|
+
'# Stage 环境变量配置',
|
|
49
|
+
'',
|
|
50
|
+
'LOGIN_SYSTEM_URL=' + renderEnvValue(input.loginSystemUrl),
|
|
51
|
+
'LOGIN_USERNAME=' + renderEnvValue(input.loginUsername),
|
|
52
|
+
'LOGIN_PASSWORD=' + renderEnvValue(input.loginPassword),
|
|
53
|
+
'',
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 渲染项目级 stage-context.md。
|
|
58
|
+
*
|
|
59
|
+
* @param input - 项目名和可选知识库路径。
|
|
60
|
+
* @returns stage-context.md 文件内容。
|
|
61
|
+
*/
|
|
62
|
+
function renderStageContext(input) {
|
|
63
|
+
const frontmatter = [
|
|
64
|
+
'---',
|
|
65
|
+
`projectName: ${renderYamlString(input.projectName)}`,
|
|
66
|
+
`projectDir: ${renderYamlString('.')}`,
|
|
67
|
+
`mode: ${renderYamlString('glue')}`,
|
|
68
|
+
`envPath: ${renderYamlString('.env')}`,
|
|
69
|
+
...(input.knowledgeRoot ? [`knowledgeRoot: ${renderYamlString(input.knowledgeRoot)}`] : []),
|
|
70
|
+
...renderYamlStringArray('codeListPaths', input.codeListPaths),
|
|
71
|
+
'---',
|
|
72
|
+
];
|
|
73
|
+
return [
|
|
74
|
+
...frontmatter,
|
|
75
|
+
'',
|
|
76
|
+
`# ${input.projectName}`,
|
|
77
|
+
'',
|
|
78
|
+
'此文件由 stage-create glue 模式生成,用于 ep-stage skills/CLI 解析项目上下文。',
|
|
79
|
+
'',
|
|
80
|
+
].join('\n');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 读取已有用户级项目索引,允许 JSON5 注释和尾逗号。
|
|
84
|
+
*
|
|
85
|
+
* @param projectIndexPath - 用户级项目索引路径。
|
|
86
|
+
* @returns 解析后的项目索引。
|
|
87
|
+
*/
|
|
88
|
+
function readProjectIndex(projectIndexPath) {
|
|
89
|
+
if (!fs.existsSync(projectIndexPath)) {
|
|
90
|
+
return { projects: [] };
|
|
91
|
+
}
|
|
92
|
+
const parsed = JSON5.parse(fs.readFileSync(projectIndexPath, 'utf8'));
|
|
93
|
+
return { projects: parsed.projects ?? [] };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 校验 glue 模式运行所需的必填输入。
|
|
97
|
+
*
|
|
98
|
+
* @param input - glue 项目上下文输入。
|
|
99
|
+
* @throws 当登录地址、用户名或密码为空时抛出错误。
|
|
100
|
+
*/
|
|
101
|
+
export function validateRequiredContextInput(input) {
|
|
102
|
+
if (!input.loginSystemUrl.trim()) {
|
|
103
|
+
throw new Error('登录页面地址不能为空');
|
|
104
|
+
}
|
|
105
|
+
if (!input.loginUsername.trim()) {
|
|
106
|
+
throw new Error('登录用户名不能为空');
|
|
107
|
+
}
|
|
108
|
+
if (!input.loginPassword.trim()) {
|
|
109
|
+
throw new Error('登录密码不能为空');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 渲染 .env 值,必要时使用双引号并转义特殊字符。
|
|
114
|
+
*
|
|
115
|
+
* @param value - 原始环境变量值。
|
|
116
|
+
* @returns 可写入 .env 的安全值。
|
|
117
|
+
*/
|
|
118
|
+
function renderEnvValue(value) {
|
|
119
|
+
if (!/[\s#"\\\r\n]/.test(value)) {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
return `"${value
|
|
123
|
+
.replace(/\\/g, '\\\\')
|
|
124
|
+
.replace(/"/g, '\\"')
|
|
125
|
+
.replace(/\n/g, '\\n')
|
|
126
|
+
.replace(/\r/g, '\\r')}"`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 渲染 YAML 字符串标量。
|
|
130
|
+
*
|
|
131
|
+
* @param value - 原始字符串。
|
|
132
|
+
* @returns 可放入 YAML frontmatter 的双引号标量。
|
|
133
|
+
*/
|
|
134
|
+
function renderYamlString(value) {
|
|
135
|
+
return JSON.stringify(value);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 渲染 YAML 字符串数组。
|
|
139
|
+
*
|
|
140
|
+
* @param key - 字段名。
|
|
141
|
+
* @param values - 字符串数组。
|
|
142
|
+
* @returns YAML 行数组。
|
|
143
|
+
*/
|
|
144
|
+
function renderYamlStringArray(key, values) {
|
|
145
|
+
if (values.length === 0) {
|
|
146
|
+
return [`${key}: []`];
|
|
147
|
+
}
|
|
148
|
+
return [
|
|
149
|
+
`${key}:`,
|
|
150
|
+
...values.map((value) => ` - ${renderYamlString(value)}`),
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 只在 knowledgeRoot/_docs/code_list.md 实际存在时登记 code list。
|
|
155
|
+
*
|
|
156
|
+
* @param knowledgeRoot - 可选知识库根目录。
|
|
157
|
+
* @returns 已存在的 code_list.md 路径列表。
|
|
158
|
+
*/
|
|
159
|
+
function getExistingCodeListPaths(knowledgeRoot) {
|
|
160
|
+
if (!knowledgeRoot) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const codeListPath = path.join(knowledgeRoot, '_docs', 'code_list.md');
|
|
164
|
+
return fs.existsSync(codeListPath) ? [codeListPath] : [];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 归一化可选路径,空字符串视为未提供。
|
|
168
|
+
*
|
|
169
|
+
* @param value - 用户输入的可选路径。
|
|
170
|
+
* @returns 绝对路径或 undefined。
|
|
171
|
+
*/
|
|
172
|
+
function normalizeOptionalPath(value) {
|
|
173
|
+
const trimmed = value?.trim();
|
|
174
|
+
return trimmed ? path.resolve(trimmed) : undefined;
|
|
175
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@epoint-testtech/stage-create",
|
|
3
|
+
"version": "0.0.3-alpha.1",
|
|
4
|
+
"description": "Epoint Stage — Playwright 脚手架 CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"stage-create": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^12.0.0",
|
|
19
|
+
"json5": "^2.2.3",
|
|
20
|
+
"prompts": "^2.4.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^25.9.1",
|
|
24
|
+
"@types/prompts": "^2.4.9",
|
|
25
|
+
"tsx": "^4.20.0",
|
|
26
|
+
"typescript": "^6.0.3"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"test": "pnpm run build && node --test test/**/*.test.mjs",
|
|
31
|
+
"dev": "tsx src/index.ts"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Stage 环境变量配置
|
|
2
|
+
# 复制此文件为 .env 并填写实际值
|
|
3
|
+
|
|
4
|
+
# 登录页面地址
|
|
5
|
+
LOGIN_SYSTEM_URL=
|
|
6
|
+
|
|
7
|
+
# 登录用户名
|
|
8
|
+
LOGIN_USERNAME=
|
|
9
|
+
|
|
10
|
+
# 登录密码
|
|
11
|
+
LOGIN_PASSWORD=
|
|
12
|
+
|
|
13
|
+
# 回放稳定性配置,可按项目情况调整
|
|
14
|
+
STAGE_ACTION_TIMEOUT_MS=30000
|
|
15
|
+
STAGE_NAVIGATION_TIMEOUT_MS=60000
|
|
16
|
+
STAGE_EXPECT_TIMEOUT_MS=30000
|
|
17
|
+
STAGE_TEST_TIMEOUT_MS=1800000
|
|
18
|
+
STAGE_SLOW_MO_MS=500
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "playwright test",
|
|
8
|
+
"test:headed": "playwright test --headed",
|
|
9
|
+
"test:debug": "playwright test --debug",
|
|
10
|
+
"report": "playwright show-report"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@epoint-testtech/stage-core": "{{stageCoreDependency}}",
|
|
14
|
+
"dotenv": "^17.2.3"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@playwright/test": "^1.60.0",
|
|
18
|
+
"typescript": "^6.0.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const configDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
dotenv.config({ path: path.resolve(configDir, '.env') });
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
testDir: './src/tests',
|
|
12
|
+
fullyParallel: true,
|
|
13
|
+
forbidOnly: !!process.env.CI,
|
|
14
|
+
retries: process.env.CI ? 2 : 0,
|
|
15
|
+
workers: process.env.CI ? 1 : undefined,
|
|
16
|
+
// 双报告并存:原生 HTML(查用例/trace/截图)+ stage-core 流程执行报告(流程视角,解释跳过/续测)。
|
|
17
|
+
// 编排器「报告」入口:流程报告 → /report,原生报告 → /pw-report。
|
|
18
|
+
reporter: [
|
|
19
|
+
['html', { open: 'never' }],
|
|
20
|
+
['@epoint-testtech/stage-core/reporter', { outputDir: 'stage-report', stateDir: '.stage-state' }],
|
|
21
|
+
],
|
|
22
|
+
timeout: 30 * 60 * 1000,
|
|
23
|
+
|
|
24
|
+
use: {
|
|
25
|
+
baseURL: process.env.LOGIN_SYSTEM_URL,
|
|
26
|
+
ignoreHTTPSErrors: true,
|
|
27
|
+
actionTimeout: 10_000,
|
|
28
|
+
trace: 'on-first-retry',
|
|
29
|
+
headless: !!(process.env.CI || process.env.HEADLESS === 'true'),
|
|
30
|
+
launchOptions: { slowMo: 500 },
|
|
31
|
+
viewport: { width: 1800, height: 900 },
|
|
32
|
+
locale: 'zh-CN',
|
|
33
|
+
screenshot: 'only-on-failure',
|
|
34
|
+
video: 'retain-on-failure',
|
|
35
|
+
permissions: ['clipboard-read'],
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
projects: [
|
|
39
|
+
{
|
|
40
|
+
name: 'chromium',
|
|
41
|
+
use: { ...devices['Desktop Chrome'], viewport: { width: 1800, height: 900 } },
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
cd "$(dirname "$0")"
|
|
5
|
+
|
|
6
|
+
echo "==> 检查 pnpm"
|
|
7
|
+
if ! command -v pnpm >/dev/null 2>&1; then
|
|
8
|
+
if command -v corepack >/dev/null 2>&1; then
|
|
9
|
+
corepack enable
|
|
10
|
+
corepack prepare pnpm@10.33.0 --activate
|
|
11
|
+
else
|
|
12
|
+
echo "未检测到 pnpm/corepack。请先安装 Node.js 18+,再重新运行本脚本。"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
echo "==> 安装项目依赖"
|
|
18
|
+
pnpm install
|
|
19
|
+
|
|
20
|
+
if [ ! -f .env ] && [ -f .env.example ]; then
|
|
21
|
+
echo "==> 创建 .env"
|
|
22
|
+
cp .env.example .env
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
echo "==> 安装 Playwright 浏览器"
|
|
26
|
+
npx playwright install
|
|
27
|
+
|
|
28
|
+
echo ""
|
|
29
|
+
echo "环境初始化完成。请按需编辑 .env,然后运行:pnpm test"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
cd /d "%~dp0"
|
|
4
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows.ps1"
|
|
5
|
+
if errorlevel 1 (
|
|
6
|
+
echo.
|
|
7
|
+
echo Setup FAILED. See the error above.
|
|
8
|
+
pause
|
|
9
|
+
exit /b 1
|
|
10
|
+
)
|
|
11
|
+
echo.
|
|
12
|
+
echo Setup completed. Edit .env if needed, then run: pnpm test
|
|
13
|
+
pause
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
$ErrorActionPreference = "Stop"
|
|
2
|
+
# 让 Write-Host 的中文在控制台正常显示(PowerShell 5.1 默认 GBK 输出)
|
|
3
|
+
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
|
|
4
|
+
Set-Location $PSScriptRoot
|
|
5
|
+
|
|
6
|
+
Write-Host "==> 检查 pnpm"
|
|
7
|
+
if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) {
|
|
8
|
+
if (Get-Command corepack -ErrorAction SilentlyContinue) {
|
|
9
|
+
corepack enable
|
|
10
|
+
corepack prepare pnpm@10.33.0 --activate
|
|
11
|
+
} else {
|
|
12
|
+
Write-Host "未检测到 pnpm/corepack。请先安装 Node.js 18+,再重新运行本脚本。"
|
|
13
|
+
exit 1
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Write-Host "==> 安装项目依赖"
|
|
18
|
+
pnpm install
|
|
19
|
+
|
|
20
|
+
if ((-not (Test-Path ".env")) -and (Test-Path ".env.example")) {
|
|
21
|
+
Write-Host "==> 创建 .env"
|
|
22
|
+
Copy-Item ".env.example" ".env"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Write-Host "==> 安装 Playwright 浏览器"
|
|
26
|
+
npx playwright install
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Stage 环境变量配置
|
|
2
|
+
# 复制此文件为 .env 并填写实际值
|
|
3
|
+
|
|
4
|
+
# 登录页面地址
|
|
5
|
+
LOGIN_SYSTEM_URL=
|
|
6
|
+
|
|
7
|
+
# 登录用户名
|
|
8
|
+
LOGIN_USERNAME=
|
|
9
|
+
|
|
10
|
+
# 登录密码
|
|
11
|
+
LOGIN_PASSWORD=
|
|
12
|
+
|
|
13
|
+
# 回放稳定性配置,可按项目情况调整
|
|
14
|
+
STAGE_ACTION_TIMEOUT_MS=30000
|
|
15
|
+
STAGE_NAVIGATION_TIMEOUT_MS=60000
|
|
16
|
+
STAGE_EXPECT_TIMEOUT_MS=30000
|
|
17
|
+
STAGE_TEST_TIMEOUT_MS=1800000
|
|
18
|
+
STAGE_SLOW_MO_MS=500
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "playwright test",
|
|
8
|
+
"test:headed": "playwright test --headed",
|
|
9
|
+
"test:debug": "playwright test --debug",
|
|
10
|
+
"report": "playwright show-report"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@epoint-testtech/stage-core": "{{stageCoreDependency}}",
|
|
14
|
+
"@epoint-testtech/ep-stage-skill": "{{epStageSkillDependency}}",
|
|
15
|
+
"dotenv": "^17.2.3"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@playwright/test": "^1.60.0",
|
|
19
|
+
"typescript": "^6.0.3"
|
|
20
|
+
}
|
|
21
|
+
}
|