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.
- package/CHANGELOG.md +8 -0
- package/README.md +9 -0
- package/package.json +1 -1
- package/remote-agent/index.ts +5 -1
- package/server/appium-recorder/appium-runner.ts +65 -18
- package/server/appium-recorder/repository.ts +32 -1
- package/server/appium-recorder/routes.ts +21 -2
- package/server/appium-recorder/script-export.ts +11 -3
- package/server/appium-recorder/variable-store.ts +22 -0
- package/server/appium-recorder/variables.ts +89 -0
- package/src/appium-recorder/AppiumPage.vue +59 -18
- package/src/appium-recorder/api.ts +23 -2
- package/src/appium-recorder/components/FlowStepEditor.vue +28 -2
- package/src/appium-recorder/components/NestedConditionBranches.vue +2 -2
- package/src/appium-recorder/components/PresetVariables.vue +109 -0
- package/src/appium-recorder/components/RecordedSteps.vue +1 -2
- package/src/appium-recorder/components/ScriptParameterSettings.vue +31 -0
- package/src/appium-recorder/components/VariableExtractionSettings.vue +17 -0
- package/src/appium-recorder/components/VariableTable.vue +43 -0
- package/src/appium-recorder/flow-graph.ts +23 -10
- package/src/appium-recorder/flow-labels.ts +4 -1
- package/src/appium-recorder/types.ts +7 -0
- package/src/appium-recorder/variables.ts +34 -0
|
@@ -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
|
-
|
|
550
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|