Skip to content

Commit ec331b3

Browse files
committed
refactor(testing-framework): type uiAgent options via core connection types
`UIAgentConfig.options` was a hand-rolled `Record<string, unknown>`, disconnected from the real agent launcher inputs and forcing `as unknown as Parameters<...>` casts in the factory. Make `UIAgentConfig` a discriminated union keyed by `type`, with each variant's `options` typed against the canonical connection types from `@midscene/core` (`WebConnectionOpt` / `AndroidConnectionOpt` / ...). `UIAgentType` is derived from the union; web's `url` is required. Also drop the now-unnecessary `agent as unknown as Agent` cast in the web factory: `PuppeteerAgent` is `@midscene/core`'s `Agent<PuppeteerWebPage>`, so once options are typed it is directly assignable to `Agent` — the cast was only needed because the prior `Record` casts poisoned the return type. Updates config test and RFC §2/§2.1. Validation: nx build core, testing-framework build + tsc + 33 unit tests, pnpm lint.
1 parent a2cf59f commit ec331b3

4 files changed

Lines changed: 49 additions & 29 deletions

File tree

packages/testing-framework/src/config/types.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,37 @@
22
* `midscene.config.ts` schema (RFC §2). Environment / target lives here, never
33
* in the case YAML.
44
*/
5+
import type {
6+
AndroidConnectionOpt,
7+
ComputerConnectionOpt,
8+
IOSConnectionOpt,
9+
WebConnectionOpt,
10+
} from '@midscene/core';
511
import type { Agent } from '@midscene/core/agent';
612
import type { AgentOpt } from '@midscene/core/agent';
713
import type { GeneralAgentAdapter } from '../general-agent/types';
814
import type { RuntimeNode } from '../runtime';
915

10-
/** Platforms the framework can build a UI Agent for out of the box. */
11-
export type UIAgentType = 'web' | 'android' | 'ios' | 'computer';
12-
1316
/** Shared UI Agent behavior parameters (aiActContext, generateReport, ...). */
1417
export type UIAgentOptions = AgentOpt;
1518

16-
/** Configuration-style UI Agent: framework builds it from `type` + `options`. */
17-
export interface UIAgentConfig {
18-
type: UIAgentType;
19-
/** Platform connection parameters (url, deviceId, ...). */
20-
options?: Record<string, unknown>;
21-
}
19+
/**
20+
* Configuration-style UI Agent: the framework builds the agent from `type` +
21+
* `options`. `options` is the platform connection target, typed against the
22+
* canonical per-platform connection types from `@midscene/core`
23+
* (`WebConnectionOpt` / `AndroidConnectionOpt` / ...). Those are the pure
24+
* "how to reach the target" shapes the agent launchers consume — agent
25+
* behavior is expressed separately via `uiAgentOptions`. Keeping `options`
26+
* bound to core means it can never drift from the launcher inputs.
27+
*/
28+
export type UIAgentConfig =
29+
| { type: 'web'; options: WebConnectionOpt }
30+
| { type: 'android'; options?: AndroidConnectionOpt }
31+
| { type: 'ios'; options?: IOSConnectionOpt }
32+
| { type: 'computer'; options?: ComputerConnectionOpt };
33+
34+
/** Platforms the framework can build a UI Agent for out of the box. */
35+
export type UIAgentType = UIAgentConfig['type'];
2236

2337
/** Context passed to a programmatic UI Agent factory. */
2438
export interface UIAgentFactoryCtx {

packages/testing-framework/src/ui-agent/factory.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* (programmatic). This module resolves both into a live Midscene UI Agent plus
66
* an optional cleanup hook.
77
*/
8+
import type { AndroidConnectionOpt, WebConnectionOpt } from '@midscene/core';
89
import type { Agent } from '@midscene/core/agent';
910
import type { UIAgent, UIAgentConfig, UIAgentOptions } from '../config/types';
1011

@@ -46,9 +47,9 @@ async function createFromConfig(
4647
): Promise<ResolvedUIAgent> {
4748
switch (config.type) {
4849
case 'web':
49-
return createWebAgent(config, uiAgentOptions);
50+
return createWebAgent(config.options, uiAgentOptions);
5051
case 'android':
51-
return createAndroidAgent(config, uiAgentOptions);
52+
return createAndroidAgent(config.options, uiAgentOptions);
5253
case 'ios':
5354
case 'computer':
5455
throw new Error(
@@ -62,11 +63,10 @@ async function createFromConfig(
6263
}
6364

6465
async function createWebAgent(
65-
config: UIAgentConfig,
66+
options: WebConnectionOpt,
6667
uiAgentOptions: UIAgentOptions | undefined,
6768
): Promise<ResolvedUIAgent> {
68-
const options = (config.options ?? {}) as Record<string, unknown>;
69-
if (!options.url) {
69+
if (!options?.url) {
7070
throw new Error('[midscene] uiAgent.type "web" requires `options.url`.');
7171
}
7272

@@ -80,14 +80,12 @@ async function createWebAgent(
8080
}
8181

8282
const { agent, freeFn } = await mod.puppeteerAgentForTarget(
83-
options as unknown as Parameters<typeof mod.puppeteerAgentForTarget>[0],
84-
uiAgentOptions as unknown as Parameters<
85-
typeof mod.puppeteerAgentForTarget
86-
>[1],
83+
options,
84+
uiAgentOptions,
8785
);
8886

8987
return {
90-
agent: agent as unknown as Agent,
88+
agent,
9189
cleanup: async () => {
9290
for (const free of freeFn) {
9391
try {
@@ -101,10 +99,10 @@ async function createWebAgent(
10199
}
102100

103101
async function createAndroidAgent(
104-
config: UIAgentConfig,
102+
options: AndroidConnectionOpt | undefined,
105103
uiAgentOptions: UIAgentOptions | undefined,
106104
): Promise<ResolvedUIAgent> {
107-
const options = (config.options ?? {}) as Record<string, unknown>;
105+
const env = options ?? {};
108106
// `@midscene/android` is an optional peer; load it loosely so the framework
109107
// does not hard-depend on it.
110108
const spec = '@midscene/android';
@@ -122,10 +120,9 @@ async function createAndroidAgent(
122120
);
123121
}
124122

125-
const deviceId = options.deviceId as string | undefined;
126-
const agent = await mod.agentFromAdbDevice(deviceId, {
127-
...(uiAgentOptions as object),
128-
...options,
123+
const agent = await mod.agentFromAdbDevice(env.deviceId, {
124+
...uiAgentOptions,
125+
...env,
129126
});
130127

131128
return {

packages/testing-framework/tests/unit-test/config.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ describe('defineMidsceneConfig', () => {
3737

3838
it('throws without testDir', () => {
3939
expect(() =>
40-
// @ts-expect-error intentionally missing
41-
defineMidsceneConfig({ uiAgent: { type: 'web' } }),
40+
// @ts-expect-error intentionally missing testDir
41+
defineMidsceneConfig({
42+
uiAgent: { type: 'web', options: { url: 'https://x.test' } },
43+
}),
4244
).toThrow(/testDir/);
4345
});
4446
});

rfcs/0001-v2-testing-framework-phase0.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,15 @@ import { defineMidsceneConfig } from '@midscene/testing-framework';
7979
8080
export default defineMidsceneConfig({
8181
// —— 运行目标:单字段 uiAgent,容纳配置式与编程式(见 §2.1)——
82+
// 配置式按 type 判别,options 直接复用 @midscene/core 的逐平台「连接类型」
83+
// (WebConnectionOpt / AndroidConnectionOpt / IOSConnectionOpt /
84+
// ComputerConnectionOpt),即从 env 类型里剥掉 agent 行为与 yaml 配置后的
85+
// 纯连接契约,与 agent launcher 入参同源,不再是手写的 Record。
8286
uiAgent:
83-
| { type: 'web' | 'android' | 'ios' | 'computer'; options: Record<string, unknown> }
87+
| { type: 'web'; options: WebConnectionOpt }
88+
| { type: 'android'; options?: AndroidConnectionOpt }
89+
| { type: 'ios'; options?: IOSConnectionOpt }
90+
| { type: 'computer'; options?: ComputerConnectionOpt }
8491
| ((ctx: UIAgentFactoryCtx) => Promise<{ agent: Agent }>);
8592
8693
// —— 用例发现 ——
@@ -120,7 +127,7 @@ export default defineMidsceneConfig({
120127
- 值是**对象** → 配置式:框架据 `type + options` 创建 UI Agent。
121128
- 值是**函数** → 编程式:项目完全掌控构造。
122129

123-
两者唯一的 key,类型层就是 union,从根上消除"两套运行目标定义"的气味。`options`(平台连接参数,如 url / deviceId)与 `uiAgentOptions`(Agent 行为,如 aiActContext / generateReport)是两类不同的东西,都保留。
130+
两者唯一的 key,类型层就是 union,从根上消除"两套运行目标定义"的气味。`options`(平台连接参数,如 url / deviceId)与 `uiAgentOptions`(Agent 行为,如 aiActContext / generateReport)是两类不同的东西,都保留。`options` 不是手写的 `Record`,而是按 `type` 判别后落到 `@midscene/core` 暴露的逐平台「连接类型」`WebConnectionOpt` / `AndroidConnectionOpt` / … 上——这些是从对应 env 类型派生、剥掉 agent 行为与 yaml 配置后的纯连接契约(web 的 `url` 因此是必填)。改 core 类型这里会立即感知,且不再把 agent-opt / output 等无关字段混进连接参数。
124131

125132
**配置式样例:**
126133

0 commit comments

Comments
 (0)