@midscene/test 1.16.0 → 1.16.2

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 (39) hide show
  1. package/dist/es/{1~77.mjs → 1~731.mjs} +9 -9
  2. package/dist/es/1~731.mjs.map +1 -0
  3. package/dist/es/cli/index.mjs +686 -167
  4. package/dist/es/cli/index.mjs.map +1 -1
  5. package/dist/es/index.mjs +45 -1
  6. package/dist/es/index.mjs.map +1 -1
  7. package/dist/es/test-cli.mjs +1863 -206
  8. package/dist/es/test-cli.mjs.map +1 -1
  9. package/dist/lib/cli/index.js +677 -155
  10. package/dist/lib/cli/index.js.map +1 -1
  11. package/dist/lib/index.js +45 -1
  12. package/dist/lib/index.js.map +1 -1
  13. package/dist/lib/test-cli.js +1854 -203
  14. package/dist/lib/test-cli.js.map +1 -1
  15. package/dist/templates/test/android/.env.example +1 -1
  16. package/dist/templates/test/android/README.md +3 -1
  17. package/dist/templates/test/android/README.zh.md +3 -1
  18. package/dist/templates/test/android/midscene.config.ts +28 -31
  19. package/dist/templates/test/computer/.env.example +1 -1
  20. package/dist/templates/test/computer/README.md +3 -1
  21. package/dist/templates/test/computer/README.zh.md +3 -1
  22. package/dist/templates/test/computer/midscene.config.ts +31 -34
  23. package/dist/templates/test/harmony/.env.example +1 -1
  24. package/dist/templates/test/harmony/README.md +3 -1
  25. package/dist/templates/test/harmony/README.zh.md +3 -1
  26. package/dist/templates/test/harmony/midscene.config.ts +28 -31
  27. package/dist/templates/test/ios/.env.example +1 -1
  28. package/dist/templates/test/ios/README.md +3 -1
  29. package/dist/templates/test/ios/README.zh.md +3 -1
  30. package/dist/templates/test/ios/midscene.config.ts +28 -31
  31. package/dist/templates/test/web/.env.example +1 -1
  32. package/dist/templates/test/web/README.md +17 -1
  33. package/dist/templates/test/web/README.zh.md +17 -1
  34. package/dist/templates/test/web/cases/todo.yaml +2 -0
  35. package/dist/templates/test/web/midscene.config.ts +34 -12
  36. package/dist/types/cli/index.d.ts +107 -11
  37. package/dist/types/index.d.ts +6 -3
  38. package/package.json +14 -13
  39. package/dist/es/1~77.mjs.map +0 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  这是参考 `midscene-example` TodoMVC Test Runner 用例的 Web 版 Midscene Test 项目。直接使用 Playwright 的示例请看 [playwright-demo](https://github.com/web-infra-dev/midscene-example/tree/main/playwright-demo)。
4
4
 
5
- 如果创建时跳过安装,执行 `{{INSTALL_COMMAND}}`。安装后会自动生成 `midscene-node-spec.web.md`;若禁用了安装脚本,手动执行 `{{RUN_NODES_COMMAND}}`。复制 `.env.example` 为 `.env`,按[模型配置指南](https://midscenejs.com/model-common-config.html)配置模型。
5
+ 如果创建时跳过安装,执行 `{{INSTALL_COMMAND}}`。安装后会自动生成 `midscene-node-spec.web.md`;若禁用了安装脚本,手动执行 `{{RUN_NODES_COMMAND}}`。复制 `.env.example` 为 `.env`,按[模型配置指南](https://midscenejs.com/model-api-setup.html)配置模型。
6
6
 
7
7
  配置后,在项目目录执行 `{{VERIFY_MODEL_COMMAND}}`,检查模型连接和 Midscene 兼容性。这一步不需要浏览器或设备;若检查失败,请先参考[模型调试与可观测性](https://midscenejs.com/model-debugging-observability.html)排查,再运行测试。
8
8
 
@@ -11,3 +11,19 @@
11
11
  `cases/todo.yaml` 使用在线 React TodoMVC。每条用例先由 `todo.setup` 打开 All 视图、清空页面存储并重新加载,通过 DOM 确认列表为空,再由 `todo.seed` 添加三个任务;第一条用例校验初始化状态,第二条用 AI 删除和完成任务,再校验最终状态。`todo.captureState` 在每条用例后记录页面状态。运行时需要能访问 TodoMVC 网站。
12
12
 
13
13
  执行 `{{TEST_COMMAND}}` 运行测试。Node 列表见 `midscene-node-spec.web.md`;修改配置中的 Node 后执行 `{{RUN_NODES_COMMAND}}` 刷新。用例串行运行,首次失败即停止后续用例(`test.bail: 1`);失败时仍会保存报告并关闭浏览器。报告位于 `midscene_run/report/`。
14
+
15
+ ## 准备工作如何执行
16
+
17
+ | 阶段 | 次数 | 本例中的工作 |
18
+ | --- | --- | --- |
19
+ | `setup`(项目配置) | 项目一次 | 启动共享 Browser |
20
+ | `fileSetup`(项目配置) | 每份文件一次 | 创建独立 BrowserContext、Page,返回 Node 使用的 `context` |
21
+ | `beforeAll`(YAML) | 文件用例开始前一次 | 设置视口;首次调用 `getAgent` 创建 Agent,后续 Node 复用 |
22
+
23
+ `fileSetup` 自动应用到每份文件;`beforeAll` 由各文件声明。业务 Node `todo.setup` 与配置项 `setup` 无关。
24
+
25
+ ## 注意事项
26
+
27
+ - 提高 `test.maxConcurrentFiles` 可并发运行文件,文件内用例仍顺序执行。
28
+ - 重试不重跑上述三个阶段;`beforeEach` 负责重置业务状态。
29
+ - 文件结束后先销毁 Agent、关闭 BrowserContext;所有文件清理完再关闭 Browser。后端账号和数据需自行隔离。
@@ -1,8 +1,10 @@
1
+ # fileSetup has prepared this file's page; getAgent creates its Agent on first use.
1
2
  beforeAll:
2
3
  - setViewportSize:
3
4
  width: 1280
4
5
  height: 720
5
6
 
7
+ # Reset business state for every case attempt, including retries.
6
8
  beforeEach:
7
9
  - todo.setup: {}
8
10
  - todo.seed:
@@ -1,26 +1,34 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import type { NodeExecutionContext } from '@midscene/test';
3
- import { defineProjectSetup, defineTestProject } from '@midscene/test/config';
3
+ import {
4
+ defineFileSetup,
5
+ defineProjectSetup,
6
+ defineTestProject,
7
+ } from '@midscene/test/config';
4
8
  import { createMidsceneNodes } from '@midscene/test/midscene';
5
9
  import { PlaywrightAgent } from '@midscene/web/playwright/agent';
6
10
  import { config as loadEnv } from 'dotenv';
7
- import { type Page, chromium } from 'playwright';
11
+ import { type Browser, type Page, chromium } from 'playwright';
8
12
  import { createTodoNodes } from './nodes/todo';
9
13
 
10
14
  loadEnv({ path: fileURLToPath(new URL('.env', import.meta.url)) });
11
15
 
12
- interface ProjectContext {
16
+ interface FileContext {
13
17
  page: Page;
14
18
  agent?: PlaywrightAgent;
15
19
  }
16
20
 
17
- const getAgent = ({
18
- context,
19
- }: NodeExecutionContext<unknown, ProjectContext>) => {
21
+ interface ProjectContext {
22
+ browser: Browser;
23
+ }
24
+
25
+ // Create the Agent on first use; reuse it within this file.
26
+ const getAgent = ({ context }: NodeExecutionContext<unknown, FileContext>) => {
20
27
  context.agent ??= new PlaywrightAgent(context.page);
21
28
  return context.agent;
22
29
  };
23
30
 
31
+ // Once per project: keep the shared Browser until every file has cleaned up.
24
32
  const setup = defineProjectSetup<ProjectContext>({
25
33
  name: 'web',
26
34
  async setup({ env, onTeardown }) {
@@ -28,8 +36,21 @@ const setup = defineProjectSetup<ProjectContext>({
28
36
  headless: env.HEADLESS !== 'false',
29
37
  });
30
38
  onTeardown(() => browser.close());
31
- const page = await browser.newPage();
32
- const context: ProjectContext = { page };
39
+ return { browser };
40
+ },
41
+ });
42
+
43
+ // Before each file's beforeAll: create its session, page, and Node context.
44
+ const fileSetup = defineFileSetup<FileContext, ProjectContext>({
45
+ name: 'web-file',
46
+ async setup({ projectContext, signal, onTeardown }) {
47
+ if (!projectContext)
48
+ throw new Error('Web Project setup did not create a browser.');
49
+ const browserContext = await projectContext.browser.newContext();
50
+ onTeardown(() => browserContext.close());
51
+ signal.throwIfAborted();
52
+ const page = await browserContext.newPage();
53
+ const context: FileContext = { page };
33
54
  onTeardown(async () => {
34
55
  await context.agent?.destroy();
35
56
  });
@@ -37,20 +58,21 @@ const setup = defineProjectSetup<ProjectContext>({
37
58
  },
38
59
  });
39
60
 
40
- export default defineTestProject<ProjectContext>({
61
+ export default defineTestProject<FileContext, ProjectContext>({
41
62
  projects: [
42
63
  {
43
64
  name: 'web',
44
65
  setup,
66
+ fileSetup,
45
67
  files: { include: ['cases/**/*.{yaml,yml}'] },
46
68
  },
47
69
  ],
48
70
  nodes: [
49
- ...createTodoNodes<ProjectContext>({ getPage: (context) => context.page }),
50
- ...createMidsceneNodes<ProjectContext>({
71
+ ...createTodoNodes<FileContext>({ getPage: (context) => context.page }),
72
+ ...createMidsceneNodes<FileContext>({
51
73
  agentClass: PlaywrightAgent,
52
74
  getAgent,
53
75
  }),
54
76
  ],
55
- test: { maxConcurrency: 1, bail: 1, testTimeout: 600_000 },
77
+ test: { maxConcurrentFiles: 1, bail: 1, testTimeout: 600_000 },
56
78
  });
@@ -1,7 +1,11 @@
1
1
  import { Awaitable } from '@midscene/core/internal/test-runner';
2
+ import type { CaseRunOutcome as CaseRunOutcome_2 } from '@midscene/core/internal/test-runner';
3
+ import type { CaseRunResult as CaseRunResult_2 } from '@midscene/core/internal/test-runner';
2
4
  import { CommonAgentTestRunnerApi } from '@midscene/core/agent/test';
3
5
  import type * as Core from '@midscene/core/internal/test-runner';
4
6
  import type { ExecutionProjectDefinition as ExecutionProjectDefinition_2 } from '@midscene/core/internal/test-runner';
7
+ import { FileSetupContext } from '@midscene/core/internal/test-runner';
8
+ import { FileSetupDefinition } from '@midscene/core/internal/test-runner';
5
9
  import type { IExecutionDump } from '@midscene/core';
6
10
  import { JsonPrimitive } from '@midscene/core/internal/test-runner';
7
11
  import { JsonValue } from '@midscene/core/internal/test-runner';
@@ -12,9 +16,13 @@ import { ProjectSetupDefinition } from '@midscene/core/internal/test-runner';
12
16
  import { ProjectTeardown } from '@midscene/core/internal/test-runner';
13
17
  import { ProjectTeardownContext } from '@midscene/core/internal/test-runner';
14
18
  import { ResolvedExecutionProject } from '@midscene/core/internal/test-runner';
19
+ import type { StepExecutionInfo } from '@midscene/core/internal/test-runner';
20
+ import type { StepRunResult as StepRunResult_2 } from '@midscene/core/internal/test-runner';
15
21
  import { TestFileSelection } from '@midscene/core/internal/test-runner';
16
22
  import { TestRunReportDocumentPlan } from '@midscene/core';
23
+ import type { TestRunReportMetrics } from '@midscene/core';
17
24
  import { TestTagSelection } from '@midscene/core/internal/test-runner';
25
+ import type { WorkflowDocumentRunResult as WorkflowDocumentRunResult_2 } from '@midscene/core/internal/test-runner';
18
26
  import { WorkflowError } from '@midscene/core/internal/test-runner';
19
27
  import { WorkflowReportSource } from '@midscene/core/internal/test-runner';
20
28
 
@@ -33,33 +41,41 @@ export declare const createTestRunId: (date?: Date, uuid?: `${string}-${string}-
33
41
 
34
42
  export declare const DEFAULT_TEST_FILE_SELECTION: TestFileSelection;
35
43
 
44
+ export declare const defineFileSetup: <TContext, TProjectContext = unknown>(definition: FileSetupDefinition<TContext, TProjectContext>) => FileSetupDefinition<TContext, TProjectContext>;
45
+
36
46
  declare type DefineNodeOptions<TInput = unknown, TData = unknown, TContext = unknown> = Omit<Core.DefineNodeOptions<TInput, TData, TContext>, 'execute'> & {
37
47
  execute(ctx: NodeExecutionContext<TInput, TContext>): Core.NodeExecutionReturn<TData>;
38
48
  };
39
49
 
40
50
  export declare const defineProjectSetup: <TProjectContext>(definition: ProjectSetupDefinition<TProjectContext>) => ProjectSetupDefinition<TProjectContext>;
41
51
 
42
- export declare const defineTestProject: <TContext = undefined>(definition: TestProjectDefinition<TContext>) => TestProjectDefinition<TContext>;
52
+ export declare const defineTestProject: <TContext = undefined, TProjectContext = TContext>(definition: TestProjectDefinition<TContext, TProjectContext>) => TestProjectDefinition<TContext, TProjectContext>;
43
53
 
44
54
  export declare const discoverTestConfig: (projectRoot: string) => string | undefined;
45
55
 
46
56
  export declare const discoverTestFiles: (projectRoot: string, selection?: TestFileSelection) => string[];
47
57
 
48
- export declare type ExecutionProjectDefinition<TContext = unknown> = Omit<ExecutionProjectDefinition_2<TContext>, 'nodes'> & {
58
+ export declare type ExecutionProjectDefinition<TContext = unknown, TProjectContext = TContext> = Omit<ExecutionProjectDefinition_2<TProjectContext>, 'nodes' | 'fileSetup'> & {
59
+ fileSetup?: FileSetupDefinition<TContext, TProjectContext>;
49
60
  nodes?: readonly NodeDefinition<any, any, TContext>[];
50
61
  };
51
62
 
63
+ export { FileSetupContext }
64
+
65
+ export { FileSetupDefinition }
66
+
52
67
  export { JsonPrimitive }
53
68
 
54
69
  export { JsonValue }
55
70
 
56
- export declare interface LoadedExecutionProject<TProjectContext = unknown> extends ResolvedExecutionProject<TProjectContext> {
71
+ export declare interface LoadedExecutionProject<TContext = unknown, TProjectContext = TContext> extends ResolvedExecutionProject<TProjectContext> {
72
+ readonly fileSetup?: FileSetupDefinition<TContext, TProjectContext>;
57
73
  /** Effective Nodes: globals plus project-local overrides. */
58
74
  readonly nodes: NodeRegistry;
59
75
  }
60
76
 
61
- export declare interface LoadedTestProject<TContext = undefined> {
62
- projects: readonly LoadedExecutionProject<TContext>[];
77
+ export declare interface LoadedTestProject<TContext = undefined, TProjectContext = TContext> {
78
+ projects: readonly LoadedExecutionProject<TContext, TProjectContext>[];
63
79
  hasExplicitProjects: boolean;
64
80
  test: ResolvedTestOptions;
65
81
  output: ResolvedTestOutputDefinition;
@@ -67,7 +83,7 @@ export declare interface LoadedTestProject<TContext = undefined> {
67
83
  resolveNode(name: string): NodeDefinition<any, any, TContext> | undefined;
68
84
  }
69
85
 
70
- export declare function loadTestProject<TContext = undefined>(configPath?: string): Promise<LoadedTestProject<TContext>>;
86
+ export declare function loadTestProject<TContext = undefined, TProjectContext = TContext>(configPath?: string): Promise<LoadedTestProject<TContext, TProjectContext>>;
71
87
 
72
88
  declare interface MidsceneExecutionRef {
73
89
  id?: string;
@@ -126,7 +142,10 @@ export { ProjectTeardownContext }
126
142
  export { ResolvedExecutionProject }
127
143
 
128
144
  export declare interface ResolvedTestOptions {
129
- maxConcurrency: number;
145
+ maxConcurrentFiles: number | null;
146
+ maxConcurrentProjects: number | null;
147
+ setupTimeout: number;
148
+ teardownTimeout: number;
130
149
  bail: number;
131
150
  testTimeout: number;
132
151
  }
@@ -161,7 +180,14 @@ export declare interface TestExecutionProjectRunResult {
161
180
  export { TestFileSelection }
162
181
 
163
182
  export declare interface TestOptions {
164
- maxConcurrency?: number;
183
+ /** Global active files, including preparation and cleanup. Default 1; null is unlimited. */
184
+ maxConcurrentFiles?: number | null;
185
+ /** Projects from setup through completed cleanup. Default 1; null is unlimited. */
186
+ maxConcurrentProjects?: number | null;
187
+ /** Milliseconds for each Project setup/fileSetup, including device waits. */
188
+ setupTimeout?: number;
189
+ /** Milliseconds per cleanup scope; shared deadline during abnormal shutdown. */
190
+ teardownTimeout?: number;
165
191
  bail?: number;
166
192
  testTimeout?: number;
167
193
  }
@@ -182,9 +208,10 @@ export declare interface TestProjectCollectionError {
182
208
  error: WorkflowError;
183
209
  }
184
210
 
185
- export declare interface TestProjectDefinition<TContext = undefined> {
186
- setup?: ProjectSetupDefinition<TContext>;
187
- projects?: readonly ExecutionProjectDefinition<TContext>[];
211
+ export declare interface TestProjectDefinition<TContext = undefined, TProjectContext = TContext> {
212
+ setup?: ProjectSetupDefinition<TProjectContext>;
213
+ fileSetup?: FileSetupDefinition<TContext, TProjectContext>;
214
+ projects?: readonly ExecutionProjectDefinition<TContext, TProjectContext>[];
188
215
  test?: TestOptions;
189
216
  output?: TestOutputDefinition;
190
217
  nodes?: readonly NodeDefinition<any, any, TContext>[];
@@ -197,9 +224,12 @@ export declare interface TestProjectRunOptions {
197
224
  resultDir?: string;
198
225
  projectNames?: readonly string[];
199
226
  onProgress?(message: string): void;
227
+ onEvent?(event: TestReporterEvent): void;
200
228
  }
201
229
 
202
230
  export declare interface TestProjectRunResult {
231
+ /** Shared report usage, including every attempt and lifecycle phase. */
232
+ metrics?: TestRunReportMetrics;
203
233
  operationLog?: {
204
234
  path: string;
205
235
  snapshotAt: string;
@@ -241,6 +271,72 @@ export declare interface TestProjectRunSummary {
241
271
  projectFailures: number;
242
272
  }
243
273
 
274
+ /** Structured presentation facts; execution and persistence do not depend on a reporter. */
275
+ declare type TestReporterEvent = {
276
+ type: 'project-end';
277
+ project: TestExecutionProjectRunResult;
278
+ } | {
279
+ type: 'project-phase';
280
+ projectName: string;
281
+ phase: 'setup' | 'running' | 'teardown';
282
+ } | {
283
+ type: 'run-start';
284
+ projects: number;
285
+ documents: number;
286
+ cases: number;
287
+ projectRoot?: string;
288
+ configPath?: string;
289
+ startedAt?: string;
290
+ maxConcurrentProjects?: number | null;
291
+ maxConcurrentFiles?: number | null;
292
+ configuredModels?: readonly {
293
+ name: string;
294
+ intents: readonly string[];
295
+ }[];
296
+ files?: readonly {
297
+ documentId: string;
298
+ projectName: string;
299
+ sourcePath: string;
300
+ cases: number;
301
+ }[];
302
+ } | {
303
+ type: 'document-start';
304
+ documentId: string;
305
+ projectName: string;
306
+ sourcePath: string;
307
+ cases: number;
308
+ } | {
309
+ type: 'document-end';
310
+ document: WorkflowDocumentRunResult_2;
311
+ cases: readonly CaseRunOutcome_2[];
312
+ durationMs: number;
313
+ } | {
314
+ type: 'step-start';
315
+ projectName: string;
316
+ info: StepExecutionInfo;
317
+ } | {
318
+ type: 'step-end';
319
+ projectName: string;
320
+ info: StepExecutionInfo;
321
+ result?: StepRunResult_2;
322
+ } | {
323
+ type: 'case-attempt-end';
324
+ attempt: CaseRunResult_2;
325
+ maxAttempts: number;
326
+ } | {
327
+ type: 'case-progress';
328
+ outcome: CaseRunOutcome_2;
329
+ documentAttemptIndex: number;
330
+ } | {
331
+ type: 'case-end';
332
+ outcome: CaseRunOutcome_2;
333
+ documentRetries?: number;
334
+ } | {
335
+ type: 'document-retry';
336
+ document: WorkflowDocumentRunResult_2;
337
+ maxAttempts: number;
338
+ };
339
+
244
340
  export { TestTagSelection }
245
341
 
246
342
  export declare const validateTestFileSelection: (value: unknown, label?: string) => TestFileSelection | undefined;
@@ -49,6 +49,7 @@ import { StepValue } from '@midscene/core/internal/test-runner';
49
49
  import { TestFileSelection } from '@midscene/core/internal/test-runner';
50
50
  import { TestRunReportDocumentPlan } from '@midscene/core';
51
51
  import type { TestRunReportDump } from '@midscene/core';
52
+ import type { TestRunReportMetrics } from '@midscene/core';
52
53
  import type { TestRunReportSourceIndex } from '@midscene/core';
53
54
  import { TestTagSelection } from '@midscene/core/internal/test-runner';
54
55
  import { WorkflowDocumentDefinition } from '@midscene/core/internal/test-runner';
@@ -129,7 +130,7 @@ export declare const createCaseRunner: <TContext = undefined>(options?: CaseRunn
129
130
 
130
131
  export declare const createDocumentRuntime: <TContext = undefined>(document: CollectedWorkflowDocument, options: CreateDocumentRuntimeOptions<TContext>) => WorkflowDocumentRuntime<TContext>;
131
132
 
132
- export declare type CreateDocumentRuntimeOptions<TContext = undefined> = Omit<Core.CreateDocumentRuntimeOptions<TContext>, 'documentSetup' | 'documentAttemptIndex' | 'resolveNode' | 'onStepResult' | 'onResult'> & {
133
+ export declare type CreateDocumentRuntimeOptions<TContext = undefined> = Omit<Core.CreateDocumentRuntimeOptions<TContext>, 'documentSetup' | 'setupTimeoutMs' | 'teardownTimeoutMs' | 'documentAttemptIndex' | 'resolveNode' | 'onStepResult' | 'onResult'> & {
133
134
  resolveNode(name: string): NodeDefinition<any, any, TContext>;
134
135
  onStepResult?: StepResultHandler;
135
136
  onResult?(result: WorkflowDocumentRunResult): Core.Awaitable<unknown>;
@@ -262,7 +263,7 @@ export declare interface ResolveWorkflowVariablesOptions {
262
263
  location: VariableResolutionLocation;
263
264
  }
264
265
 
265
- export declare type RunCollectedCaseOptions<TContext = undefined> = Omit<Core.RunCollectedCaseOptions<TContext>, 'reportScopeId' | 'resolveNode' | 'beforeEach' | 'afterEach' | 'onStepResult' | 'onResult'> & {
266
+ export declare type RunCollectedCaseOptions<TContext = undefined> = Omit<Core.RunCollectedCaseOptions<TContext>, 'reportScopeId' | 'teardownTimeoutMs' | 'resolveNode' | 'beforeEach' | 'afterEach' | 'onStepResult' | 'onResult'> & {
266
267
  resolveNode(name: string): NodeDefinition<any, any, TContext>;
267
268
  beforeEach?: readonly NormalizedStep[];
268
269
  afterEach?: readonly NormalizedStep[];
@@ -272,7 +273,7 @@ export declare type RunCollectedCaseOptions<TContext = undefined> = Omit<Core.Ru
272
273
 
273
274
  export declare const runWorkflowDocument: <TContext = undefined>(document: CollectedWorkflowDocument, options: RunWorkflowDocumentOptions<TContext>) => Promise<WorkflowDocumentExecutionResult>;
274
275
 
275
- export declare type RunWorkflowDocumentOptions<TContext = undefined> = Omit<Core.RunWorkflowDocumentOptions<TContext>, 'documentSetup' | 'documentAttemptIndex' | 'resolveCaseReportScopeId' | 'resolveNode' | 'onStepResult' | 'onCaseStart' | 'isFatalError' | 'onCaseResult' | 'onCaseOutcome' | 'onDocumentResult' | 'createCaseRunId' | 'createDocumentRunId'> & {
276
+ export declare type RunWorkflowDocumentOptions<TContext = undefined> = Omit<Core.RunWorkflowDocumentOptions<TContext>, 'documentSetup' | 'setupTimeoutMs' | 'teardownTimeoutMs' | 'documentAttemptIndex' | 'resolveCaseReportScopeId' | 'resolveNode' | 'onStepResult' | 'onCaseStart' | 'isFatalError' | 'onCaseResult' | 'onCaseOutcome' | 'onDocumentResult' | 'createCaseRunId' | 'createDocumentRunId'> & {
276
277
  resolveNode(name: string): NodeDefinition<any, any, TContext>;
277
278
  onStepResult?: StepResultHandler;
278
279
  onCaseStart?(collectedCase: CollectedCase): Core.Awaitable<void>;
@@ -337,6 +338,8 @@ declare interface TestProjectCollectionError {
337
338
  }
338
339
 
339
340
  declare interface TestProjectRunResult {
341
+ /** Shared report usage, including every attempt and lifecycle phase. */
342
+ metrics?: TestRunReportMetrics;
340
343
  operationLog?: {
341
344
  path: string;
342
345
  snapshotAt: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@midscene/test",
3
3
  "description": "Midscene Test: an extensible YAML testing framework and Node SDK.",
4
- "version": "1.16.0",
4
+ "version": "1.16.2",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/web-infra-dev/midscene.git",
@@ -54,12 +54,13 @@
54
54
  "http-server": "14.1.1",
55
55
  "js-yaml": "4.1.0",
56
56
  "lodash.merge": "4.6.2",
57
+ "string-width": "4.2.3",
57
58
  "tinyglobby": "0.2.15",
58
59
  "tsx": "^4.19.2",
59
60
  "yargs": "17.7.3",
60
61
  "zod": "^3.25.1",
61
- "@midscene/core": "1.16.0",
62
- "@midscene/shared": "1.16.0"
62
+ "@midscene/core": "1.16.2",
63
+ "@midscene/shared": "1.16.2"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@rslib/core": "^0.18.3",
@@ -71,20 +72,20 @@
71
72
  "puppeteer": "24.6.0",
72
73
  "typescript": "^5.8.3",
73
74
  "vitest": "3.0.5",
74
- "@midscene/android": "1.16.0",
75
- "@midscene/computer": "1.16.0",
76
- "@midscene/ios": "1.16.0",
77
- "@midscene/web": "1.16.0",
78
- "@midscene/harmony": "1.16.0"
75
+ "@midscene/computer": "1.16.2",
76
+ "@midscene/ios": "1.16.2",
77
+ "@midscene/web": "1.16.2",
78
+ "@midscene/harmony": "1.16.2",
79
+ "@midscene/android": "1.16.2"
79
80
  },
80
81
  "peerDependencies": {
81
82
  "playwright": "^1.45.0",
82
83
  "puppeteer": "24.6.0",
83
- "@midscene/android": "1.16.0",
84
- "@midscene/computer": "1.16.0",
85
- "@midscene/harmony": "1.16.0",
86
- "@midscene/ios": "1.16.0",
87
- "@midscene/web": "1.16.0"
84
+ "@midscene/android": "1.16.2",
85
+ "@midscene/harmony": "1.16.2",
86
+ "@midscene/ios": "1.16.2",
87
+ "@midscene/computer": "1.16.2",
88
+ "@midscene/web": "1.16.2"
88
89
  },
89
90
  "peerDependenciesMeta": {
90
91
  "@midscene/android": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"1~77.mjs","sources":["../../src/cli/dotenv-loader.ts","../../src/cli/model-command.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport dotenv from 'dotenv';\n\nexport interface DotenvLoadOptions {\n dotenvOverride?: boolean;\n dotenvDebug?: boolean;\n cwd?: string;\n log?: (message: string) => void;\n}\n\nexport function loadDotenvConfig(options: DotenvLoadOptions = {}) {\n const dotEnvConfigFile = join(options.cwd ?? process.cwd(), '.env');\n if (!existsSync(dotEnvConfigFile)) {\n return;\n }\n\n options.log?.(` Env file: ${dotEnvConfigFile}`);\n dotenv.config({\n path: dotEnvConfigFile,\n debug: options.dotenvDebug,\n override: options.dotenvOverride,\n });\n}\n","import { runConnectivityTest } from '@midscene/core';\nimport {\n type IModelConfig,\n type TIntent,\n globalModelConfigManager,\n} from '@midscene/shared/env';\nimport chalk from 'chalk';\nimport { loadDotenvConfig } from './dotenv-loader';\n\nconst DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';\nconst MODEL_VERIFY_SEPARATOR = '────────────────────────────────────────';\nconst modelCommandUsage = (commandName: string) => `Usage:\n ${commandName} model verify\n\nVerify model connectivity and Midscene compatibility for default, locate, planning, and insight models.\nReads .env from the current working directory, overriding shell environment variables.\nShows endpoints, model names, request counts, and per-check results with duration.\nReturns exit code 0 on success and 1 on failure.\n\nOptions:\n --help, -h Show this help\n`;\n\ninterface ModelCommandIO {\n stdout: (message: string) => void;\n stderr: (message: string) => void;\n}\n\ninterface ModelCommandDeps {\n loadDotenv: () => void;\n getModelConfig: (intent: TIntent) => IModelConfig;\n verifyModel: typeof runConnectivityTest;\n}\n\nexport interface CurlCommandItem {\n intents: TIntent[];\n curl: string;\n usesDefaultBaseURL: boolean;\n}\n\nfunction assertNoModelVerifyOptions(args: string[], commandName: string) {\n for (const arg of args) {\n throw new Error(`Unknown option for ${commandName} model verify: ${arg}`);\n }\n}\n\nfunction formatTable(headers: string[], rows: string[][]): string {\n const widths = headers.map((header, index) =>\n Math.max(header.length, ...rows.map((row) => row[index].length)),\n );\n const formatRow = (row: string[]) =>\n row\n .map((cell, index) => cell.padEnd(widths[index]))\n .join(' ')\n .trimEnd();\n return [formatRow(headers), ...rows.map(formatRow)].join('\\n');\n}\n\nfunction displayEndpoint(endpoint: string): string {\n try {\n const url = new URL(endpoint);\n url.username = '';\n url.password = '';\n url.search = '';\n url.hash = '';\n return url.toString();\n } catch {\n return '(invalid endpoint URL)';\n }\n}\n\nfunction shellSingleQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nfunction buildChatCompletionsUrl(baseURL?: string): string {\n const normalizedBaseURL = (baseURL || DEFAULT_OPENAI_BASE_URL).replace(\n /\\/+$/,\n '',\n );\n if (normalizedBaseURL.endsWith('/chat/completions')) {\n return normalizedBaseURL;\n }\n return `${normalizedBaseURL}/chat/completions`;\n}\n\nfunction buildCurlCommand(modelConfig: IModelConfig): string {\n const payload = {\n model: modelConfig.modelName,\n messages: [{ role: 'user', content: 'What is 1+1?' }],\n };\n\n return [\n `curl -X POST ${shellSingleQuote(buildChatCompletionsUrl(modelConfig.openaiBaseURL))} \\\\`,\n ` -H ${shellSingleQuote(`Authorization: Bearer ${modelConfig.openaiApiKey || ''}`)} \\\\`,\n ` -H ${shellSingleQuote('Content-Type: application/json')} \\\\`,\n ` -d ${shellSingleQuote(JSON.stringify(payload, null, 2))}`,\n ].join('\\n');\n}\n\nfunction buildCurlDedupKey(modelConfig: IModelConfig): string {\n return JSON.stringify({\n baseURL: modelConfig.openaiBaseURL || DEFAULT_OPENAI_BASE_URL,\n apiKey: modelConfig.openaiApiKey || '',\n modelName: modelConfig.modelName,\n });\n}\n\nexport function buildModelVerifyCurlCommands(\n configs: Array<{ intent: TIntent; modelConfig: IModelConfig }>,\n): CurlCommandItem[] {\n const commandMap = new Map<string, CurlCommandItem>();\n\n for (const item of configs) {\n const key = buildCurlDedupKey(item.modelConfig);\n const existing = commandMap.get(key);\n if (existing) {\n existing.intents.push(item.intent);\n continue;\n }\n commandMap.set(key, {\n intents: [item.intent],\n curl: buildCurlCommand(item.modelConfig),\n usesDefaultBaseURL: !item.modelConfig.openaiBaseURL,\n });\n }\n\n return [...commandMap.values()];\n}\n\nfunction formatModelVerifyFailureOutput(\n message: string | undefined,\n curlCommands: CurlCommandItem[],\n): string {\n const details = message?.trim() || 'No failure details were generated.';\n const curlSection = curlCommands\n .map((item) => {\n const baseUrlNote = item.usesDefaultBaseURL\n ? ' (base URL not configured; using OpenAI SDK default)'\n : '';\n return chalk.gray(\n `# ${item.intents.join(', ')}${baseUrlNote}\\n${item.curl}`,\n );\n })\n .join('\\n\\n');\n\n return [\n chalk.red.bold('❌ Model verify failed with messages:'),\n MODEL_VERIFY_SEPARATOR,\n '',\n details,\n '',\n MODEL_VERIFY_SEPARATOR,\n 'Generated curl requests for basic API connectivity:',\n 'If the error is a basic connectivity issue, use these requests to test the base URL, API key, and model name directly.',\n 'These commands contain your API key. Do not share them publicly.',\n '',\n curlSection,\n ].join('\\n');\n}\n\nfunction getDefaultDeps(io: ModelCommandIO): ModelCommandDeps {\n return {\n loadDotenv: () => {\n loadDotenvConfig({\n dotenvDebug: true,\n dotenvOverride: true,\n log: io.stdout,\n });\n },\n getModelConfig: (intent) => globalModelConfigManager.getModelConfig(intent),\n verifyModel: runConnectivityTest,\n };\n}\n\nasync function runModelVerifyCommand(\n args: string[],\n deps: ModelCommandDeps,\n io: ModelCommandIO,\n commandName: string,\n): Promise<number> {\n try {\n if (args.includes('--help') || args.includes('-h')) {\n io.stdout(modelCommandUsage(commandName));\n return 0;\n }\n\n assertNoModelVerifyOptions(args, commandName);\n io.stdout(\n 'Model verify started. This usually completes within 30 seconds.',\n );\n deps.loadDotenv();\n io.stdout('');\n\n const defaultModelConfig = deps.getModelConfig('default');\n const locateModelConfig = deps.getModelConfig('locate');\n const planningModelConfig = deps.getModelConfig('planning');\n const insightModelConfig = deps.getModelConfig('insight');\n const curlCommands = buildModelVerifyCurlCommands([\n { intent: 'default', modelConfig: defaultModelConfig },\n { intent: 'locate', modelConfig: locateModelConfig },\n { intent: 'planning', modelConfig: planningModelConfig },\n { intent: 'insight', modelConfig: insightModelConfig },\n ]);\n\n const baseURLs = new Map<string, number>();\n const modelRows = (\n [\n ['planning', planningModelConfig, 'Text response'],\n ['insight', insightModelConfig, 'Image understanding'],\n ['locate', locateModelConfig, 'AI locate'],\n ['default', defaultModelConfig, 'Text response'],\n ] as const\n ).map(([intent, config, check]) => {\n const baseURL = config.openaiExtraConfig?.baseURL ?? config.openaiBaseURL;\n const endpoint = baseURL\n ? displayEndpoint(String(baseURL))\n : 'provider default';\n if (!baseURLs.has(endpoint)) baseURLs.set(endpoint, baseURLs.size + 1);\n return [intent, config.modelName, check, `[${baseURLs.get(endpoint)}]`];\n });\n io.stdout(\n formatTable(['Model role', 'Model', 'Check', 'Endpoint'], modelRows),\n );\n io.stdout('');\n for (const [endpoint, index] of baseURLs) {\n io.stdout(`[${index}] Base URL: ${endpoint}`);\n }\n io.stdout('\\nRunning 4 checks...\\n');\n\n const result = await deps.verifyModel({\n defaultModelConfig,\n locateModelConfig,\n planningModelConfig,\n insightModelConfig,\n });\n\n const checkLabels = {\n text: 'Text response',\n vision: 'Image understanding',\n aiLocate: 'AI locate',\n };\n io.stdout(\n formatTable(\n ['Model role', 'Check', 'Result', 'Requests', 'Time'],\n result.checks.map((check) => [\n check.intent,\n checkLabels[check.name],\n check.passed ? 'PASS' : 'FAIL',\n String(check.requestCount),\n `${check.durationMs} ms`,\n ]),\n ),\n );\n const requestEndpoints = new Map<string, Set<string>>();\n for (const check of result.checks) {\n for (const endpoint of check.endpoints) {\n const display = displayEndpoint(endpoint);\n if (!requestEndpoints.has(display))\n requestEndpoints.set(display, new Set());\n requestEndpoints.get(display)!.add(check.intent);\n }\n if (!check.passed && check.message)\n io.stdout(`\\n${check.intent}: ${check.message}`);\n }\n if (requestEndpoints.size > 0) {\n io.stdout('\\nRequest endpoints:');\n for (const [endpoint, intents] of requestEndpoints) {\n io.stdout(` ${[...intents].join(', ')}: ${endpoint}`);\n }\n }\n io.stdout('');\n const passedChecks = result.checks.filter((check) => check.passed).length;\n io.stdout(\n `Requests sent: ${result.requestCount}. Checks: ${passedChecks} passed, ${result.checks.length - passedChecks} failed.`,\n );\n\n if (result.passed) {\n io.stdout('✅ Model verify passed.');\n return 0;\n }\n\n io.stderr(formatModelVerifyFailureOutput(result.message, curlCommands));\n return 1;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n io.stderr(\n [\n MODEL_VERIFY_SEPARATOR,\n '❌ Model verify failed with messages:',\n '',\n message,\n MODEL_VERIFY_SEPARATOR,\n ].join('\\n'),\n );\n return 1;\n }\n}\n\nexport async function runModelCommand(\n rawArgs: string[],\n deps?: Partial<ModelCommandDeps>,\n io: ModelCommandIO = {\n stdout: console.log,\n stderr: console.error,\n },\n commandName = 'midscene-test',\n): Promise<number> {\n const [, action, ...restArgs] = rawArgs;\n const mergedDeps = {\n ...getDefaultDeps(io),\n ...deps,\n };\n\n if (!action || action === '--help' || action === '-h') {\n io.stdout(modelCommandUsage(commandName));\n return 0;\n }\n\n if (action === 'verify') {\n return runModelVerifyCommand(restArgs, mergedDeps, io, commandName);\n }\n\n if (action === 'eval') {\n io.stderr(\n `${commandName} model eval is not implemented yet. It is reserved for future model evaluation suites.`,\n );\n return 1;\n }\n\n io.stderr(\n `Unknown ${commandName} model command: ${action}\\n\\n${modelCommandUsage(commandName)}`,\n );\n return 1;\n}\n\nexport { loadDotenvConfig, type DotenvLoadOptions } from './dotenv-loader';\n"],"names":["loadDotenvConfig","options","dotEnvConfigFile","join","process","existsSync","dotenv","DEFAULT_OPENAI_BASE_URL","MODEL_VERIFY_SEPARATOR","modelCommandUsage","commandName","assertNoModelVerifyOptions","args","arg","Error","formatTable","headers","rows","widths","header","index","Math","row","formatRow","cell","displayEndpoint","endpoint","url","URL","shellSingleQuote","value","buildChatCompletionsUrl","baseURL","normalizedBaseURL","buildCurlCommand","modelConfig","payload","JSON","buildCurlDedupKey","buildModelVerifyCurlCommands","configs","commandMap","Map","item","key","existing","formatModelVerifyFailureOutput","message","curlCommands","details","curlSection","baseUrlNote","chalk","getDefaultDeps","io","intent","globalModelConfigManager","runConnectivityTest","runModelVerifyCommand","deps","defaultModelConfig","locateModelConfig","planningModelConfig","insightModelConfig","baseURLs","modelRows","config","check","String","result","checkLabels","requestEndpoints","display","Set","intents","passedChecks","error","runModelCommand","rawArgs","console","action","restArgs","mergedDeps"],"mappings":";;;;;;;;;;;;;;;QAWO,SAASA,iBAAiBC,UAA6B,CAAC,CAAC;YAC9D,MAAMC,mBAAmBC,AAAAA,IAAAA,oBAAAA,IAAAA,AAAAA,EAAKF,QAAQ,GAAG,IAAIG,QAAQ,GAAG,IAAI;YAC5D,IAAI,CAACC,AAAAA,IAAAA,kBAAAA,UAAAA,AAAAA,EAAWH,mBACd;YAGFD,QAAQ,GAAG,GAAG,CAAC,aAAa,EAAEC,kBAAkB;YAChDI,OAAO,MAAM,CAAC;gBACZ,MAAMJ;gBACN,OAAOD,QAAQ,WAAW;gBAC1B,UAAUA,QAAQ,cAAc;YAClC;QACF;QCdA,MAAMM,0BAA0B;QAChC,MAAMC,yBAAyB;QAC/B,MAAMC,oBAAoB,CAACC,cAAwB,CAAC;EAClD,EAAEA,YAAY;;;;;;;;;AAShB,CAAC;QAmBD,SAASC,2BAA2BC,IAAc,EAAEF,WAAmB;YACrE,KAAK,MAAMG,OAAOD,KAChB,MAAM,IAAIE,MAAM,CAAC,mBAAmB,EAAEJ,YAAY,eAAe,EAAEG,KAAK;QAE5E;QAEA,SAASE,YAAYC,OAAiB,EAAEC,IAAgB;YACtD,MAAMC,SAASF,QAAQ,GAAG,CAAC,CAACG,QAAQC,QAClCC,KAAK,GAAG,CAACF,OAAO,MAAM,KAAKF,KAAK,GAAG,CAAC,CAACK,MAAQA,GAAG,CAACF,MAAM,CAAC,MAAM;YAEhE,MAAMG,YAAY,CAACD,MACjBA,IACG,GAAG,CAAC,CAACE,MAAMJ,QAAUI,KAAK,MAAM,CAACN,MAAM,CAACE,MAAM,GAC9C,IAAI,CAAC,MACL,OAAO;YACZ,OAAO;gBAACG,UAAUP;mBAAaC,KAAK,GAAG,CAACM;aAAW,CAAC,IAAI,CAAC;QAC3D;QAEA,SAASE,gBAAgBC,QAAgB;YACvC,IAAI;gBACF,MAAMC,MAAM,IAAIC,IAAIF;gBACpBC,IAAI,QAAQ,GAAG;gBACfA,IAAI,QAAQ,GAAG;gBACfA,IAAI,MAAM,GAAG;gBACbA,IAAI,IAAI,GAAG;gBACX,OAAOA,IAAI,QAAQ;YACrB,EAAE,OAAM;gBACN,OAAO;YACT;QACF;QAEA,SAASE,iBAAiBC,KAAa;YACrC,OAAO,CAAC,CAAC,EAAEA,MAAM,OAAO,CAAC,MAAM,SAAS,CAAC,CAAC;QAC5C;QAEA,SAASC,wBAAwBC,OAAgB;YAC/C,MAAMC,oBAAqBD,AAAAA,CAAAA,WAAWzB,uBAAsB,EAAG,OAAO,CACpE,QACA;YAEF,IAAI0B,kBAAkB,QAAQ,CAAC,sBAC7B,OAAOA;YAET,OAAO,GAAGA,kBAAkB,iBAAiB,CAAC;QAChD;QAEA,SAASC,iBAAiBC,WAAyB;YACjD,MAAMC,UAAU;gBACd,OAAOD,YAAY,SAAS;gBAC5B,UAAU;oBAAC;wBAAE,MAAM;wBAAQ,SAAS;oBAAe;iBAAE;YACvD;YAEA,OAAO;gBACL,CAAC,aAAa,EAAEN,iBAAiBE,wBAAwBI,YAAY,aAAa,GAAG,GAAG,CAAC;gBACzF,CAAC,KAAK,EAAEN,iBAAiB,CAAC,sBAAsB,EAAEM,YAAY,YAAY,IAAI,IAAI,EAAE,GAAG,CAAC;gBACxF,CAAC,KAAK,EAAEN,iBAAiB,kCAAkC,GAAG,CAAC;gBAC/D,CAAC,KAAK,EAAEA,iBAAiBQ,KAAK,SAAS,CAACD,SAAS,MAAM,KAAK;aAC7D,CAAC,IAAI,CAAC;QACT;QAEA,SAASE,kBAAkBH,WAAyB;YAClD,OAAOE,KAAK,SAAS,CAAC;gBACpB,SAASF,YAAY,aAAa,IAAI5B;gBACtC,QAAQ4B,YAAY,YAAY,IAAI;gBACpC,WAAWA,YAAY,SAAS;YAClC;QACF;QAEO,SAASI,6BACdC,OAA8D;YAE9D,MAAMC,aAAa,IAAIC;YAEvB,KAAK,MAAMC,QAAQH,QAAS;gBAC1B,MAAMI,MAAMN,kBAAkBK,KAAK,WAAW;gBAC9C,MAAME,WAAWJ,WAAW,GAAG,CAACG;gBAChC,IAAIC,UAAU;oBACZA,SAAS,OAAO,CAAC,IAAI,CAACF,KAAK,MAAM;oBACjC;gBACF;gBACAF,WAAW,GAAG,CAACG,KAAK;oBAClB,SAAS;wBAACD,KAAK,MAAM;qBAAC;oBACtB,MAAMT,iBAAiBS,KAAK,WAAW;oBACvC,oBAAoB,CAACA,KAAK,WAAW,CAAC,aAAa;gBACrD;YACF;YAEA,OAAO;mBAAIF,WAAW,MAAM;aAAG;QACjC;QAEA,SAASK,+BACPC,OAA2B,EAC3BC,YAA+B;YAE/B,MAAMC,UAAUF,SAAS,UAAU;YACnC,MAAMG,cAAcF,aACjB,GAAG,CAAC,CAACL;gBACJ,MAAMQ,cAAcR,KAAK,kBAAkB,GACvC,yDACA;gBACJ,OAAOS,MAAM,IAAI,CACf,CAAC,EAAE,EAAET,KAAK,OAAO,CAAC,IAAI,CAAC,QAAQQ,YAAY,EAAE,EAAER,KAAK,IAAI,EAAE;YAE9D,GACC,IAAI,CAAC;YAER,OAAO;gBACLS,MAAM,GAAG,CAAC,IAAI,CAAC;gBACf5C;gBACA;gBACAyC;gBACA;gBACAzC;gBACA;gBACA;gBACA;gBACA;gBACA0C;aACD,CAAC,IAAI,CAAC;QACT;QAEA,SAASG,eAAeC,EAAkB;YACxC,OAAO;gBACL,YAAY;oBACVtD,iBAAiB;wBACf,aAAa;wBACb,gBAAgB;wBAChB,KAAKsD,GAAG,MAAM;oBAChB;gBACF;gBACA,gBAAgB,CAACC,SAAWC,yBAAyB,cAAc,CAACD;gBACpE,aAAaE;YACf;QACF;QAEA,eAAeC,sBACb9C,IAAc,EACd+C,IAAsB,EACtBL,EAAkB,EAClB5C,WAAmB;YAEnB,IAAI;gBACF,IAAIE,KAAK,QAAQ,CAAC,aAAaA,KAAK,QAAQ,CAAC,OAAO;oBAClD0C,GAAG,MAAM,CAAC7C,kBAAkBC;oBAC5B,OAAO;gBACT;gBAEAC,2BAA2BC,MAAMF;gBACjC4C,GAAG,MAAM,CACP;gBAEFK,KAAK,UAAU;gBACfL,GAAG,MAAM,CAAC;gBAEV,MAAMM,qBAAqBD,KAAK,cAAc,CAAC;gBAC/C,MAAME,oBAAoBF,KAAK,cAAc,CAAC;gBAC9C,MAAMG,sBAAsBH,KAAK,cAAc,CAAC;gBAChD,MAAMI,qBAAqBJ,KAAK,cAAc,CAAC;gBAC/C,MAAMX,eAAeT,6BAA6B;oBAChD;wBAAE,QAAQ;wBAAW,aAAaqB;oBAAmB;oBACrD;wBAAE,QAAQ;wBAAU,aAAaC;oBAAkB;oBACnD;wBAAE,QAAQ;wBAAY,aAAaC;oBAAoB;oBACvD;wBAAE,QAAQ;wBAAW,aAAaC;oBAAmB;iBACtD;gBAED,MAAMC,WAAW,IAAItB;gBACrB,MAAMuB,YACJ;oBACE;wBAAC;wBAAYH;wBAAqB;qBAAgB;oBAClD;wBAAC;wBAAWC;wBAAoB;qBAAsB;oBACtD;wBAAC;wBAAUF;wBAAmB;qBAAY;oBAC1C;wBAAC;wBAAWD;wBAAoB;qBAAgB;iBACjD,CACD,GAAG,CAAC,CAAC,CAACL,QAAQW,QAAQC,MAAM;oBAC5B,MAAMnC,UAAUkC,OAAO,iBAAiB,EAAE,WAAWA,OAAO,aAAa;oBACzE,MAAMxC,WAAWM,UACbP,gBAAgB2C,OAAOpC,YACvB;oBACJ,IAAI,CAACgC,SAAS,GAAG,CAACtC,WAAWsC,SAAS,GAAG,CAACtC,UAAUsC,SAAS,IAAI,GAAG;oBACpE,OAAO;wBAACT;wBAAQW,OAAO,SAAS;wBAAEC;wBAAO,CAAC,CAAC,EAAEH,SAAS,GAAG,CAACtC,UAAU,CAAC,CAAC;qBAAC;gBACzE;gBACA4B,GAAG,MAAM,CACPvC,YAAY;oBAAC;oBAAc;oBAAS;oBAAS;iBAAW,EAAEkD;gBAE5DX,GAAG,MAAM,CAAC;gBACV,KAAK,MAAM,CAAC5B,UAAUN,MAAM,IAAI4C,SAC9BV,GAAG,MAAM,CAAC,CAAC,CAAC,EAAElC,MAAM,YAAY,EAAEM,UAAU;gBAE9C4B,GAAG,MAAM,CAAC;gBAEV,MAAMe,SAAS,MAAMV,KAAK,WAAW,CAAC;oBACpCC;oBACAC;oBACAC;oBACAC;gBACF;gBAEA,MAAMO,cAAc;oBAClB,MAAM;oBACN,QAAQ;oBACR,UAAU;gBACZ;gBACAhB,GAAG,MAAM,CACPvC,YACE;oBAAC;oBAAc;oBAAS;oBAAU;oBAAY;iBAAO,EACrDsD,OAAO,MAAM,CAAC,GAAG,CAAC,CAACF,QAAU;wBAC3BA,MAAM,MAAM;wBACZG,WAAW,CAACH,MAAM,IAAI,CAAC;wBACvBA,MAAM,MAAM,GAAG,SAAS;wBACxBC,OAAOD,MAAM,YAAY;wBACzB,GAAGA,MAAM,UAAU,CAAC,GAAG,CAAC;qBACzB;gBAGL,MAAMI,mBAAmB,IAAI7B;gBAC7B,KAAK,MAAMyB,SAASE,OAAO,MAAM,CAAE;oBACjC,KAAK,MAAM3C,YAAYyC,MAAM,SAAS,CAAE;wBACtC,MAAMK,UAAU/C,gBAAgBC;wBAChC,IAAI,CAAC6C,iBAAiB,GAAG,CAACC,UACxBD,iBAAiB,GAAG,CAACC,SAAS,IAAIC;wBACpCF,iBAAiB,GAAG,CAACC,SAAU,GAAG,CAACL,MAAM,MAAM;oBACjD;oBACA,IAAI,CAACA,MAAM,MAAM,IAAIA,MAAM,OAAO,EAChCb,GAAG,MAAM,CAAC,CAAC,EAAE,EAAEa,MAAM,MAAM,CAAC,EAAE,EAAEA,MAAM,OAAO,EAAE;gBACnD;gBACA,IAAII,iBAAiB,IAAI,GAAG,GAAG;oBAC7BjB,GAAG,MAAM,CAAC;oBACV,KAAK,MAAM,CAAC5B,UAAUgD,QAAQ,IAAIH,iBAChCjB,GAAG,MAAM,CAAC,CAAC,EAAE,EAAE;2BAAIoB;qBAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,EAAEhD,UAAU;gBAEzD;gBACA4B,GAAG,MAAM,CAAC;gBACV,MAAMqB,eAAeN,OAAO,MAAM,CAAC,MAAM,CAAC,CAACF,QAAUA,MAAM,MAAM,EAAE,MAAM;gBACzEb,GAAG,MAAM,CACP,CAAC,eAAe,EAAEe,OAAO,YAAY,CAAC,UAAU,EAAEM,aAAa,SAAS,EAAEN,OAAO,MAAM,CAAC,MAAM,GAAGM,aAAa,QAAQ,CAAC;gBAGzH,IAAIN,OAAO,MAAM,EAAE;oBACjBf,GAAG,MAAM,CAAC;oBACV,OAAO;gBACT;gBAEAA,GAAG,MAAM,CAACR,+BAA+BuB,OAAO,OAAO,EAAErB;gBACzD,OAAO;YACT,EAAE,OAAO4B,OAAO;gBACd,MAAM7B,UAAU6B,iBAAiB9D,QAAQ8D,MAAM,OAAO,GAAGR,OAAOQ;gBAChEtB,GAAG,MAAM,CACP;oBACE9C;oBACA;oBACA;oBACAuC;oBACAvC;iBACD,CAAC,IAAI,CAAC;gBAET,OAAO;YACT;QACF;QAEO,eAAeqE,gBACpBC,OAAiB,EACjBnB,IAAgC,EAChCL,KAAqB;YACnB,QAAQyB,QAAQ,GAAG;YACnB,QAAQA,QAAQ,KAAK;QACvB,CAAC,EACDrE,cAAc,eAAe;YAE7B,MAAM,GAAGsE,QAAQ,GAAGC,SAAS,GAAGH;YAChC,MAAMI,aAAa;gBACjB,GAAG7B,eAAeC,GAAG;gBACrB,GAAGK,IAAI;YACT;YAEA,IAAI,CAACqB,UAAUA,AAAW,aAAXA,UAAuBA,AAAW,SAAXA,QAAiB;gBACrD1B,GAAG,MAAM,CAAC7C,kBAAkBC;gBAC5B,OAAO;YACT;YAEA,IAAIsE,AAAW,aAAXA,QACF,OAAOtB,sBAAsBuB,UAAUC,YAAY5B,IAAI5C;YAGzD,IAAIsE,AAAW,WAAXA,QAAmB;gBACrB1B,GAAG,MAAM,CACP,GAAG5C,YAAY,sFAAsF,CAAC;gBAExG,OAAO;YACT;YAEA4C,GAAG,MAAM,CACP,CAAC,QAAQ,EAAE5C,YAAY,gBAAgB,EAAEsE,OAAO,IAAI,EAAEvE,kBAAkBC,cAAc;YAExF,OAAO;QACT"}