@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.
Files changed (36) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +129 -0
  3. package/dist/render-template.d.ts +10 -0
  4. package/dist/render-template.js +34 -0
  5. package/dist/stage-context.d.ts +27 -0
  6. package/dist/stage-context.js +175 -0
  7. package/package.json +33 -0
  8. package/templates/default/.env.example +18 -0
  9. package/templates/default/package.json.tpl +20 -0
  10. package/templates/default/playwright.config.ts +44 -0
  11. package/templates/default/setup-mac.sh +29 -0
  12. package/templates/default/setup-windows.cmd +13 -0
  13. package/templates/default/setup-windows.ps1 +26 -0
  14. package/templates/default/src/global.setup.ts +5 -0
  15. package/templates/default/tsconfig.json +12 -0
  16. package/templates/glue/.env.example +18 -0
  17. package/templates/glue/package.json.tpl +21 -0
  18. package/templates/glue/playwright.config.ts +56 -0
  19. package/templates/glue/src/data/stage-config.yaml.tpl +13 -0
  20. package/templates/glue/src/gap-executor/gap-executor.ts +162 -0
  21. package/templates/glue/src/gap-executor/gap-healer.ts +50 -0
  22. package/templates/glue/src/gap-executor/page-structure-observer.ts +102 -0
  23. package/templates/glue/src/gap-executor/runtime-runner.ts +817 -0
  24. package/templates/glue/src/report/glue-report.ts +855 -0
  25. package/templates/glue/src/report/run-info.ts +85 -0
  26. package/templates/glue/src/skeletons/crud.skeleton.ts +450 -0
  27. package/templates/glue/src/skeletons/export.skeleton.ts +114 -0
  28. package/templates/glue/src/skeletons/index.ts +18 -0
  29. package/templates/glue/src/skeletons/login.skeleton.ts +104 -0
  30. package/templates/glue/src/skeletons/menu.skeleton.ts +37 -0
  31. package/templates/glue/src/tests/example.spec.ts +99 -0
  32. package/templates/glue/src/web/component/anchor-types.ts +9 -0
  33. package/templates/glue/src/web/component/dataexport-component.ts +143 -0
  34. package/templates/glue/src/web/component/index.ts +2 -0
  35. package/templates/glue/src/web/component/listbox-component.ts +41 -0
  36. package/templates/glue/tsconfig.json +12 -0
@@ -0,0 +1,56 @@
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
+ const numberFromEnv = (name: string, fallback: number): number => {
11
+ const value = Number(process.env[name]);
12
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
13
+ };
14
+
15
+ const actionTimeout = numberFromEnv('STAGE_ACTION_TIMEOUT_MS', 30_000);
16
+ const navigationTimeout = numberFromEnv('STAGE_NAVIGATION_TIMEOUT_MS', 60_000);
17
+ const expectTimeout = numberFromEnv('STAGE_EXPECT_TIMEOUT_MS', 30_000);
18
+ const slowMo = numberFromEnv('STAGE_SLOW_MO_MS', 500);
19
+
20
+ export default defineConfig({
21
+ testDir: './src/tests',
22
+ fullyParallel: true,
23
+ forbidOnly: !!process.env.CI,
24
+ retries: process.env.CI ? 2 : 0,
25
+ workers: process.env.CI ? 1 : undefined,
26
+ // 双报告并存:原生 HTML(查用例/trace/截图)+ stage-core 流程执行报告(流程视角,解释跳过/续测)。
27
+ // 编排器「报告」入口:流程报告 → /report,原生报告 → /pw-report。
28
+ reporter: [
29
+ ['html', { open: 'never' }],
30
+ ['@epoint-testtech/stage-core/reporter', { outputDir: 'stage-report', stateDir: '.stage-state' }],
31
+ ],
32
+ timeout: numberFromEnv('STAGE_TEST_TIMEOUT_MS', 30 * 60 * 1000),
33
+ expect: { timeout: expectTimeout },
34
+
35
+ use: {
36
+ baseURL: process.env.LOGIN_SYSTEM_URL,
37
+ ignoreHTTPSErrors: true,
38
+ actionTimeout,
39
+ navigationTimeout,
40
+ trace: 'on-first-retry',
41
+ headless: !!(process.env.CI || process.env.HEADLESS === 'true'),
42
+ launchOptions: { slowMo },
43
+ viewport: { width: 1800, height: 900 },
44
+ locale: 'zh-CN',
45
+ screenshot: 'only-on-failure',
46
+ video: 'retain-on-failure',
47
+ permissions: ['clipboard-read'],
48
+ },
49
+
50
+ projects: [
51
+ {
52
+ name: 'chromium',
53
+ use: { ...devices['Desktop Chrome'], viewport: { width: 1800, height: 900 } },
54
+ },
55
+ ],
56
+ });
@@ -0,0 +1,13 @@
1
+ # stage-config.yaml — 项目配置
2
+ # 由 stage-create 脚手架自动生成,按项目实际情况修改
3
+
4
+ # 应用配置
5
+ 应用配置:
6
+ 应用名称: "{{projectName}}"
7
+ 应用版本: "0.0.1"
8
+
9
+ # 菜单配置(LoginPage.navigateToMenu 使用)
10
+ 菜单配置: {}
11
+ # 示例:
12
+ # "场所窗口信息管理":
13
+ # "场所窗口信息列表": "场所窗口信息管理>场所窗口信息列表"
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Gap Executor 纯函数层。
3
+ *
4
+ * 提供 resolved workflow 构建、spec 追加和运行计划生成。
5
+ * 真实页面操作(登录、菜单导航、按钮定位)由后续迭代接入。
6
+ *
7
+ * 真实 gap-executor runner 的执行顺序:
8
+ * 1. 使用 LoginPage.login() 复用登录骨架。
9
+ * 2. 使用 MenuPage.navigateToMenu(menuRoute) 进入模块页面。
10
+ * 3. 读取 planned/resolved 的 AgentInferredWorkflow。
11
+ * 4. 在真实页面中按 ActionCandidate.locator 或页面快照定位按钮。
12
+ * 5. 点击按钮后,只能基于 upstream_java_action / upstream_doc / runtime_page 证据形成断言。
13
+ * 6. 证据不足时写 needs_review,不生成通过断言。
14
+ */
15
+
16
+ import type { AgentInferredWorkflow } from '@epoint-testtech/ep-stage-skill';
17
+ import {
18
+ appendAgentWorkflowTest,
19
+ renderAgentWorkflowTest,
20
+ stableActionId,
21
+ stableWorkflowId,
22
+ } from '@epoint-testtech/ep-stage-skill';
23
+
24
+ type BuildWorkflowInput = {
25
+ moduleId: string;
26
+ pageId: string;
27
+ actionType: 'page_button' | 'row_action' | 'dialog_action';
28
+ label: string;
29
+ locator: string;
30
+ intent: string;
31
+ assertion: string;
32
+ };
33
+
34
+ type ResolvePlannedInput = {
35
+ workflow: AgentInferredWorkflow;
36
+ locator: string;
37
+ locatorStrategy: 'html_static' | 'playwright_snapshot' | 'agent_refined';
38
+ iframeSrcKeyword?: string;
39
+ exploration?: NonNullable<AgentInferredWorkflow['resolvedRuntime']>['exploration'];
40
+ };
41
+
42
+ type AppendInput = {
43
+ source: string;
44
+ moduleId: string;
45
+ suiteTitle: string;
46
+ workflow: AgentInferredWorkflow;
47
+ };
48
+
49
+ type RuntimePlanInput = {
50
+ moduleId: string;
51
+ menuRoute: string;
52
+ workflowIds: string[];
53
+ };
54
+
55
+ export function createGapExecutorRuntimePlan(input: RuntimePlanInput): {
56
+ moduleId: string;
57
+ menuRoute: string;
58
+ workflowIds: string[];
59
+ loginWith: 'LoginPage.login';
60
+ navigateWith: 'MenuPage.navigateToMenu';
61
+ evidenceSources: ['runtime_page', 'agent_reasoning'];
62
+ } {
63
+ return {
64
+ moduleId: input.moduleId,
65
+ menuRoute: input.menuRoute,
66
+ workflowIds: input.workflowIds,
67
+ loginWith: 'LoginPage.login',
68
+ navigateWith: 'MenuPage.navigateToMenu',
69
+ evidenceSources: ['runtime_page', 'agent_reasoning'],
70
+ };
71
+ }
72
+
73
+ export function buildResolvedAgentWorkflow(input: BuildWorkflowInput): AgentInferredWorkflow {
74
+ const hashInput = {
75
+ moduleId: input.moduleId,
76
+ pageId: input.pageId,
77
+ actionType: input.actionType,
78
+ label: input.label,
79
+ };
80
+ const workflowId = stableWorkflowId(hashInput);
81
+ const actionId = stableActionId(hashInput);
82
+
83
+ return {
84
+ workflowId,
85
+ workflowKind: 'custom',
86
+ executionSource: 'agent_inferred',
87
+ status: 'resolved',
88
+ businessIntent: { status: 'resolved', value: input.intent },
89
+ elementActions: { status: 'resolved', value: [`点击「${input.label}」按钮`] },
90
+ assertionExpectation: { status: 'resolved', value: input.assertion },
91
+ actionCandidates: [{
92
+ actionId,
93
+ moduleId: input.moduleId,
94
+ pageId: input.pageId,
95
+ pageRole: 'list',
96
+ actionType: input.actionType,
97
+ label: input.label,
98
+ locator: input.locator,
99
+ evidence: [],
100
+ sourceRefs: [],
101
+ }],
102
+ triggerReason: '该动作不属于 CRUD + Export 骨架覆盖范围。',
103
+ evidence: [],
104
+ reviewReason: 'gap-executor 在真实页面确认该自定义业务动作可定位。',
105
+ resolvedRuntime: {
106
+ locator: input.locator,
107
+ locatorStrategy: 'html_static',
108
+ playwrightTestTitle: `Custom:${input.label}`,
109
+ },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * 基于 planned workflow 和真实页面补证结果,构造 resolved workflow。
115
+ * @param input planned workflow 与 runtime locator 信息
116
+ * @returns 保留原业务语义与上游证据的 resolved workflow
117
+ */
118
+ export function createResolvedWorkflowFromPlanned(input: ResolvePlannedInput): AgentInferredWorkflow {
119
+ if (input.workflow.status !== 'planned' && input.workflow.status !== 'resolved') {
120
+ throw new Error(`workflow ${input.workflow.workflowId} 状态为 ${input.workflow.status},不能补齐为 resolved`);
121
+ }
122
+
123
+ const firstCandidate = input.workflow.actionCandidates[0];
124
+ if (!firstCandidate) {
125
+ throw new Error(`workflow ${input.workflow.workflowId} 缺少 actionCandidate`);
126
+ }
127
+
128
+ return {
129
+ ...input.workflow,
130
+ status: 'resolved',
131
+ actionCandidates: [
132
+ {
133
+ ...firstCandidate,
134
+ locator: input.locator,
135
+ },
136
+ ...input.workflow.actionCandidates.slice(1),
137
+ ],
138
+ reviewReason: input.workflow.reviewReason ?? 'gap-executor 在真实页面确认该自定义业务动作可定位。',
139
+ resolvedRuntime: {
140
+ locator: input.locator,
141
+ locatorStrategy: input.locatorStrategy,
142
+ iframeSrcKeyword: input.iframeSrcKeyword,
143
+ exploration: input.exploration,
144
+ playwrightTestTitle: `Custom:${firstCandidate.label}`,
145
+ },
146
+ };
147
+ }
148
+
149
+ export function appendResolvedAgentWorkflowToSpec(input: AppendInput): string {
150
+ const testSource = renderAgentWorkflowTest({
151
+ moduleId: input.moduleId,
152
+ suiteTitle: input.suiteTitle,
153
+ workflow: input.workflow,
154
+ });
155
+
156
+ return appendAgentWorkflowTest({
157
+ source: input.source,
158
+ workflowId: input.workflow.workflowId,
159
+ testTitle: input.workflow.resolvedRuntime?.playwrightTestTitle ?? input.workflow.workflowId,
160
+ testSource,
161
+ });
162
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 轻量 Gap Healer:分类失败、限制修复范围、生成 healer attempt 记录。
3
+ *
4
+ * 第一版只允许修复 4 类问题,不修改业务意图或凭空发明断言。
5
+ */
6
+
7
+ import type { AgentInferredWorkflow } from '@epoint-testtech/ep-stage-skill';
8
+
9
+ export type GapHealerFailureType =
10
+ | 'element_not_found'
11
+ | 'timeout'
12
+ | 'toast_not_found'
13
+ | 'click_intercepted'
14
+ | 'needs_review';
15
+
16
+ export type GapHealerAttempt = NonNullable<AgentInferredWorkflow['healerAttempts']>[number];
17
+
18
+ export function classifyGapFailure(error: unknown): GapHealerFailureType {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+
21
+ if (/resolved to 0 elements|not found|strict mode violation/i.test(message)) {
22
+ return 'element_not_found';
23
+ }
24
+ if (/Timeout|timed out|超时/i.test(message)) {
25
+ return 'timeout';
26
+ }
27
+ if (/提示|toast|message/i.test(message)) {
28
+ return 'toast_not_found';
29
+ }
30
+ if (/intercepts pointer events|not visible|not enabled/i.test(message)) {
31
+ return 'click_intercepted';
32
+ }
33
+
34
+ return 'needs_review';
35
+ }
36
+
37
+ export function createGapHealerAttempt(input: {
38
+ attempt: number;
39
+ failureType: Exclude<GapHealerFailureType, 'needs_review'>;
40
+ change: string;
41
+ result: GapHealerAttempt['result'];
42
+ }): GapHealerAttempt {
43
+ return {
44
+ attempt: input.attempt,
45
+ failureType: input.failureType,
46
+ change: input.change,
47
+ evidence: [],
48
+ result: input.result,
49
+ };
50
+ }
@@ -0,0 +1,102 @@
1
+ import type { Frame, Page } from '@playwright/test';
2
+
3
+ /**
4
+ * 运行时页面结构信号:静态结构在运行时是否可见。
5
+ *
6
+ * 与 ep-stage-skill 的 RuntimePageStructureSignal 结构同构,e2e 侧单独定义
7
+ * 以避免 runtime runner 反向依赖 skill 包的契约模块。
8
+ */
9
+ export type RuntimePageStructureSignal = {
10
+ selector: string;
11
+ staticPresent: boolean;
12
+ runtimeVisible: boolean;
13
+ runtimeMismatch: boolean;
14
+ evidenceText: string;
15
+ };
16
+
17
+ /**
18
+ * runtime runner 关注的三类 MiniUI 核心结构选择器。
19
+ */
20
+ const selectors = [
21
+ "div[class*='mini-datagrid']",
22
+ "div[class*='fui-condition']",
23
+ "div[class*='mini-grid-pager']",
24
+ ];
25
+
26
+ /**
27
+ * 在 runtime runner 当前已定位的业务页面内采集 MiniUI 核心结构的运行时可见性。
28
+ *
29
+ * 优先检查业务 iframe,再回退主页面与其他 iframe。staticPresent 此处恒为 true,
30
+ * 静态存在性由 page-structure 抽取阶段提供;本函数只补运行时可见性证据。
31
+ *
32
+ * @param page - runtime runner 当前页面。
33
+ * @param iframeKeyword - 契约解析出的业务 iframe src 关键字。
34
+ * @returns 三类结构选择器的运行时可见性信号。
35
+ */
36
+ export async function collectRuntimePageStructureSignals(
37
+ page: Page,
38
+ iframeKeyword?: string,
39
+ ): Promise<RuntimePageStructureSignal[]> {
40
+ const signals: RuntimePageStructureSignal[] = [];
41
+ for (const selector of selectors) {
42
+ const visibleContext = await findVisibleContext(page, selector, iframeKeyword);
43
+ const runtimeVisible = visibleContext !== undefined;
44
+ signals.push({
45
+ selector,
46
+ staticPresent: true,
47
+ runtimeVisible,
48
+ runtimeMismatch: !runtimeVisible,
49
+ evidenceText: runtimeVisible
50
+ ? `${selector} 运行时可见(${visibleContext})`
51
+ : `${selector} 在主页面和业务 iframe 中均不可见或未挂载`,
52
+ });
53
+ }
54
+ return signals;
55
+ }
56
+
57
+ /**
58
+ * 查找指定选择器在哪个运行时上下文中可见。
59
+ *
60
+ * @param page - Playwright 页面。
61
+ * @param selector - MiniUI 结构选择器。
62
+ * @param iframeKeyword - 业务 iframe src 关键字。
63
+ * @returns 可见上下文说明;不可见时返回 undefined。
64
+ */
65
+ async function findVisibleContext(
66
+ page: Page,
67
+ selector: string,
68
+ iframeKeyword?: string,
69
+ ): Promise<string | undefined> {
70
+ const contexts = listRuntimeContexts(page, iframeKeyword);
71
+ for (const context of contexts) {
72
+ const runtimeVisible = await context.target.locator(selector).first().isVisible().catch(() => false);
73
+ if (runtimeVisible) {
74
+ return context.label;
75
+ }
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ /**
81
+ * 列出运行时页面结构观察上下文,业务 iframe 优先。
82
+ *
83
+ * @param page - Playwright 页面。
84
+ * @param iframeKeyword - 业务 iframe src 关键字。
85
+ * @returns 主页面和 frame 上下文。
86
+ */
87
+ function listRuntimeContexts(
88
+ page: Page,
89
+ iframeKeyword?: string,
90
+ ): Array<{ label: string; target: Page | Frame }> {
91
+ const frames = page.frames().filter((frame) => frame !== page.mainFrame());
92
+ const businessFrames = iframeKeyword
93
+ ? frames.filter((frame) => frame.url().includes(iframeKeyword))
94
+ : frames;
95
+ const otherFrames = frames.filter((frame) => !businessFrames.includes(frame));
96
+
97
+ return [
98
+ ...businessFrames.map((frame) => ({ label: `业务 iframe: ${frame.url()}`, target: frame })),
99
+ { label: '主页面', target: page },
100
+ ...otherFrames.map((frame) => ({ label: `iframe: ${frame.url()}`, target: frame })),
101
+ ];
102
+ }