android-midscene-automation 0.1.32 → 0.1.34

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.
@@ -45,6 +45,7 @@ export type InsertAction =
45
45
  | 'waitDisappear'
46
46
  | 'waitActivity'
47
47
  | 'runScript'
48
+ | 'extractVariable'
48
49
  | 'noop'
49
50
  | 'log'
50
51
  | 'visualChangeStart'
@@ -232,6 +233,24 @@ function addUniqueLink(links: Link[], seen: Set<string>, link: Link) {
232
233
  links.push(link);
233
234
  }
234
235
 
236
+ // 与回放的默认分支返回一致;循环体的虚拟回边不参与有向无环布局。
237
+ function continuationTargetId(items: StepItem[], step: AppiumRecordedStep, seen = new Set<string>()): string {
238
+ if (seen.has(step.id)) return '';
239
+ seen.add(step.id);
240
+ if (step.flow?.successTargetId) return step.flow.successTargetId;
241
+ if (defaultKind(step) === 'condition') {
242
+ const index = items.findIndex(item => item.step.id === step.id);
243
+ const sibling = items.slice(index + 1).find(({ step: candidate }) => (
244
+ candidate.flow?.parentConditionId === step.flow?.parentConditionId
245
+ && candidate.flow?.parentBranch === step.flow?.parentBranch
246
+ ));
247
+ if (sibling) return sibling.step.id;
248
+ }
249
+ const parent = items.find(item => item.step.id === step.flow?.parentConditionId)?.step;
250
+ if (!parent || (parent.type === 'loop' && step.flow?.parentBranch === 'yes')) return '';
251
+ return continuationTargetId(items, parent, seen);
252
+ }
253
+
235
254
  function branchConnectionTargetId(items: StepItem[], condition: AppiumRecordedStep, branch: FlowBranch) {
236
255
  const branchItems = directBranchItems(items, condition.id, branch);
237
256
  if (branchItems.length) {
@@ -239,6 +258,7 @@ function branchConnectionTargetId(items: StepItem[], condition: AppiumRecordedSt
239
258
  return defaultKind(last) === 'condition' ? '' : last.flow?.successTargetId || '';
240
259
  }
241
260
  const targetId = branch === 'yes' ? condition.flow?.yesTargetId : condition.flow?.noTargetId;
261
+ if (!targetId) return condition.type === 'loop' && branch === 'yes' ? '' : continuationTargetId(items, condition);
242
262
  const target = items.find(({ step }) => step.id === targetId)?.step;
243
263
  return target && target.flow?.parentConditionId === condition.flow?.parentConditionId ? target.id : '';
244
264
  }
@@ -546,11 +566,8 @@ export function buildFlowGraph(
546
566
 
547
567
  const targetId = item.step.flow?.successTargetId;
548
568
  const explicitTarget = targetId ? stepById.get(targetId) : undefined;
549
- const explicitMainTarget = explicitTarget && (!explicitTarget.step.flow?.parentConditionId
550
- || explicitTarget.step.flow.parentConditionId !== item.step.flow?.parentConditionId)
551
- ? explicitTarget
552
- : undefined;
553
- const target = explicitMainTarget || nextSibling;
569
+ // 显式跳转优先于顺序关系,包括同一分支内的跳转;失效目标不能悄悄改走相邻节点。
570
+ const target = targetId ? explicitTarget : nextSibling || stepById.get(continuationTargetId(items, item.step));
554
571
  const insertId = addInsertNode(
555
572
  item.index,
556
573
  branch,
@@ -558,10 +575,6 @@ export function buildFlowGraph(
558
575
  );
559
576
  addVisibleLink(nodeIdForStep(item.step), insertId, branch);
560
577
  if (!target) return;
561
- if (branch && explicitMainTarget) {
562
- addVisibleLink(insertId, nodeIdForStep(target.step), branch);
563
- return;
564
- }
565
578
  addVisibleLink(insertId, nodeIdForStep(target.step), branch);
566
579
  }
567
580
 
@@ -741,7 +754,7 @@ export function buildFlowGraph(
741
754
  // 公共起点从两侧汇入,不能被最后一条分支连线拉到左列或右列。
742
755
  const joins = new Map<string, string>();
743
756
  items.forEach(({ step }) => {
744
- const target = step.flow?.successTargetId && stepById.get(step.flow.successTargetId);
757
+ const target = defaultKind(step) === 'condition' && stepById.get(continuationTargetId(items, step));
745
758
  if (defaultKind(step) === 'condition' && target && !joins.has(nodeIdForStep(target.step))) {
746
759
  joins.set(nodeIdForStep(target.step), nodeIdForStep(step));
747
760
  }
@@ -10,6 +10,7 @@ export function isBooleanCondition(step: Pick<AppiumRecordedStep, 'type'>) {
10
10
  }
11
11
 
12
12
  export function defaultFlowKind(step: AppiumRecordedStep): FlowKind {
13
+ if (step.type === 'extractVariable') return 'action';
13
14
  if (step.type === 'loop') return 'condition';
14
15
  if (step.type === 'breakLoop') return 'action';
15
16
  if (step.type === 'log' || step.type === 'openGallery' || step.type === 'endFlow') return 'action';
@@ -65,12 +66,14 @@ export function flowTypeLabel(step: AppiumRecordedStep) {
65
66
  runScript: '连接脚本',
66
67
  noop: '空节点',
67
68
  log: '输出日志',
69
+ extractVariable: '提取变量',
68
70
  visualChange: '检测画面变化',
69
71
  };
70
72
  return labelMap[step.type] || step.type;
71
73
  }
72
74
 
73
75
  export function flowStepMeta(step: AppiumRecordedStep) {
76
+ if (step.type === 'extractVariable') return `${step.extractVariable?.attribute || 'text'} → ${step.extractVariable?.name || '未设置变量名'}`;
74
77
  if (step.type === 'loop') return `最多 ${step.loop?.maxIterations ?? '?'} 次 · ${step.loop?.exitWhen === 'exists' ? '元素出现时退出' : step.loop?.exitWhen === 'notExists' ? '元素消失时退出' : '固定次数'}${step.loop?.exitWhen !== 'never' ? ` ${step.selector?.value || ''}` : ''}`;
75
78
  if (step.type === 'breakLoop') return step.breakLoopTargetId ? '退出指定循环,继续循环结束后的流程' : '退出当前循环,继续循环结束后的流程';
76
79
  if (step.type === 'log') return `${step.logPrefix ?? DEFAULT_LOG_PREFIX}:${step.value || ''}`;
@@ -103,7 +106,7 @@ export function flowStepMeta(step: AppiumRecordedStep) {
103
106
  }
104
107
  if (step.type === 'pinch') return step.pinch?.direction === 'out' ? '放大' : '缩小';
105
108
  if (step.type === 'coordinateTap') {
106
- return `${step.fallback?.centerX || ''},${step.fallback?.centerY || ''}`;
109
+ return `${step.fallback?.centerX ?? ''},${step.fallback?.centerY ?? ''}`;
107
110
  }
108
111
  if (step.type === 'assertText') return step.value || '';
109
112
  return `${step.selector?.strategy || ''} ${step.selector?.value || ''}`.trim();
@@ -1,3 +1,5 @@
1
+ import type { TestVariable, VariableExtraction, ScriptReturn } from './variables';
2
+
1
3
  export type AppiumBounds = {
2
4
  left: number;
3
5
  top: number;
@@ -99,6 +101,7 @@ export type AppiumRecordedStep = {
99
101
  | 'pinch'
100
102
  | 'runScript'
101
103
  | 'noop'
104
+ | 'extractVariable'
102
105
  | 'log'
103
106
  | 'visualChange';
104
107
  label: string;
@@ -109,6 +112,9 @@ export type AppiumRecordedStep = {
109
112
  fallback?: AppiumSelector;
110
113
  value?: string;
111
114
  logPrefix?: string;
115
+ extractVariable?: VariableExtraction;
116
+ parameters?: TestVariable[];
117
+ returns?: ScriptReturn[];
112
118
  keyCode?: number;
113
119
  timeoutMs?: number;
114
120
  longPressMode?: 'element' | 'coordinates';
@@ -158,6 +164,7 @@ export type AppiumRecordedScript = {
158
164
  appPackage: string;
159
165
  appActivity: string;
160
166
  deviceId: string;
167
+ variables?: TestVariable[];
161
168
  steps: AppiumRecordedStep[];
162
169
  createdAt: string;
163
170
  updatedAt: string;
@@ -0,0 +1,34 @@
1
+ export type TestVariable = { name: string; value: string; sensitive?: boolean };
2
+ export type VariableExtraction = { name: string; attribute: string; sensitive?: boolean };
3
+ export type ScriptReturn = { name: string; target: string };
4
+ export const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
5
+ export const sensitiveVariableName = (name: string) => /password|passwd|secret|token|api_?key|密码|令牌/i.test(name);
6
+
7
+ export function validateVariables(input: unknown): TestVariable[] {
8
+ if (input === undefined) return [];
9
+ if (!Array.isArray(input) || input.length > 200) throw new Error('变量必须为列表,最多 200 项');
10
+ const names = new Set<string>();
11
+ return input.map(item => {
12
+ if (!item || typeof item.name !== 'string' || !VARIABLE_NAME.test(item.name) || item.name.length > 80) throw new Error('变量名须以字母或下划线开头,仅含字母、数字、下划线,最长 80 字符');
13
+ if (names.has(item.name)) throw new Error(`变量名重复:${item.name}`);
14
+ names.add(item.name);
15
+ if (typeof item.value !== 'string' || item.value.length > 20000) throw new Error(`变量 ${item.name} 的值必须为文本且不超过 20000 字符`);
16
+ return { name: item.name, value: item.value, sensitive: Boolean(item.sensitive) || sensitiveVariableName(item.name) };
17
+ });
18
+ }
19
+
20
+ export function validateExtraction(input: VariableExtraction | undefined) {
21
+ if (!input || !VARIABLE_NAME.test(input.name) || !input.attribute?.trim()) throw new Error('请填写有效变量名和组件属性');
22
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.attribute)) throw new Error('组件属性名称无效');
23
+ return input;
24
+ }
25
+
26
+ export function validateReturns(input: ScriptReturn[] = []) {
27
+ if (!Array.isArray(input) || input.length > 200) throw new Error('返回值映射无效');
28
+ const targets = new Set<string>();
29
+ for (const item of input) {
30
+ if (!item || !VARIABLE_NAME.test(item.name) || !VARIABLE_NAME.test(item.target) || targets.has(item.target)) throw new Error('返回变量名或目标变量名无效、重复');
31
+ targets.add(item.target);
32
+ }
33
+ return input;
34
+ }