android-midscene-automation 0.1.13 → 0.1.14
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 +30 -0
- package/README.md +39 -51
- package/USAGE.md +1018 -0
- package/package.json +2 -1
- package/server/android-sdk.ts +104 -0
- package/server/appium-recorder/appium-runner.ts +144 -35
- package/server/appium-recorder/report.ts +138 -0
- package/server/appium-recorder/repository.ts +78 -0
- package/server/appium-recorder/routes.ts +141 -14
- package/server/config.ts +12 -0
- package/server/http-api.ts +9 -6
- package/src/App.vue +23 -23
- package/src/appium-recorder/AppiumPage.vue +403 -38
- package/src/appium-recorder/api.ts +62 -5
- package/src/appium-recorder/components/FlowStepEditor.vue +175 -0
- package/src/appium-recorder/components/RecordedSteps.vue +514 -277
- package/src/appium-recorder/types.ts +2 -0
- package/src/pages/ConfigPage.vue +28 -0
- package/src/style.css +162 -89
- package/src/types.ts +4 -0
|
@@ -34,6 +34,10 @@ export function pressAppiumDeviceKey(input: { deviceId: string; keyCode: number
|
|
|
34
34
|
return postJson<{ success: boolean }>(`${APP_BASE}/api/appium-recorder/key`, input);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function launchAppiumDeviceApp(input: { deviceId: string; packageName: string }) {
|
|
38
|
+
return postJson<{ success: boolean }>(`${APP_BASE}/api/appium-recorder/launch-app`, input);
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
export async function getAppiumScripts() {
|
|
38
42
|
const response = await fetch(`${APP_BASE}/api/appium-recorder/scripts`);
|
|
39
43
|
return readJson<{ scripts: AppiumRecordedScript[] }>(response);
|
|
@@ -56,9 +60,62 @@ export function deleteAppiumScript(id: string) {
|
|
|
56
60
|
}).then((response) => readJson<{ success: boolean }>(response));
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
export function
|
|
60
|
-
return postJson<{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
export function importAppiumScript(input: unknown) {
|
|
64
|
+
return postJson<{ script: AppiumRecordedScript }>(`${APP_BASE}/api/appium-recorder/scripts/import`, input);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function appiumScriptDownloadUrl(id: string) {
|
|
68
|
+
return `${APP_BASE}/api/appium-recorder/scripts/${encodeURIComponent(id)}/export`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type ReplayResult = {
|
|
72
|
+
success: boolean;
|
|
73
|
+
output: string;
|
|
74
|
+
reportPath?: string;
|
|
75
|
+
reportId?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type ReplayStreamEvent =
|
|
79
|
+
| { type: 'log'; line: string }
|
|
80
|
+
| ({ type: 'result' } & ReplayResult)
|
|
81
|
+
| { type: 'error'; message: string };
|
|
82
|
+
|
|
83
|
+
export async function replayAppiumScript(
|
|
84
|
+
input: { id: string; deviceId?: string },
|
|
85
|
+
onOutput?: (line: string) => void,
|
|
86
|
+
) {
|
|
87
|
+
const response = await fetch(`${APP_BASE}/api/appium-recorder/scripts/${encodeURIComponent(input.id)}/replay`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
Accept: 'application/x-ndjson',
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({ deviceId: input.deviceId }),
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok || !response.body) return readJson<ReplayResult>(response);
|
|
96
|
+
|
|
97
|
+
const reader = response.body.getReader();
|
|
98
|
+
const decoder = new TextDecoder();
|
|
99
|
+
let buffer = '';
|
|
100
|
+
let result: ReplayResult | undefined;
|
|
101
|
+
|
|
102
|
+
const consumeLine = (value: string) => {
|
|
103
|
+
if (!value.trim()) return;
|
|
104
|
+
const event = JSON.parse(value) as ReplayStreamEvent;
|
|
105
|
+
if (event.type === 'log') onOutput?.(event.line);
|
|
106
|
+
if (event.type === 'result') result = event;
|
|
107
|
+
if (event.type === 'error') throw new Error(event.message);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
while (true) {
|
|
111
|
+
const { done, value } = await reader.read();
|
|
112
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
113
|
+
const lines = buffer.split('\n');
|
|
114
|
+
buffer = lines.pop() || '';
|
|
115
|
+
lines.forEach(consumeLine);
|
|
116
|
+
if (done) break;
|
|
117
|
+
}
|
|
118
|
+
consumeLine(buffer);
|
|
119
|
+
if (!result) throw new Error('回放结束但未返回执行结果');
|
|
120
|
+
return result;
|
|
64
121
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AppiumRecordedStep, AppiumSelector } from '../types';
|
|
3
|
+
|
|
4
|
+
type FlowKind = 'action' | 'condition' | 'assertion';
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
step: AppiumRecordedStep;
|
|
8
|
+
index: number;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
update: [payload: { index: number; step: AppiumRecordedStep }];
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
function defaultKind(): FlowKind {
|
|
17
|
+
if (props.step.flow?.nodeKind) return props.step.flow.nodeKind;
|
|
18
|
+
if (props.step.type === 'assertExists' || props.step.type === 'assertText') return 'assertion';
|
|
19
|
+
return 'action';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function patchStep(patch: Partial<AppiumRecordedStep>) {
|
|
23
|
+
emit('update', {
|
|
24
|
+
index: props.index,
|
|
25
|
+
step: { ...props.step, ...patch },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function patchFlow(patch: NonNullable<AppiumRecordedStep['flow']>) {
|
|
30
|
+
patchStep({
|
|
31
|
+
flow: {
|
|
32
|
+
...(props.step.flow || {}),
|
|
33
|
+
...patch,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="appium-flow-editor">
|
|
41
|
+
<el-form label-position="top" size="small">
|
|
42
|
+
<el-form-item label="节点名称">
|
|
43
|
+
<el-input
|
|
44
|
+
:model-value="step.label"
|
|
45
|
+
:disabled="disabled"
|
|
46
|
+
@update:model-value="patchStep({ label: String($event) })"
|
|
47
|
+
/>
|
|
48
|
+
</el-form-item>
|
|
49
|
+
<el-form-item label="备注">
|
|
50
|
+
<el-input
|
|
51
|
+
:model-value="step.note || ''"
|
|
52
|
+
:disabled="disabled"
|
|
53
|
+
maxlength="100"
|
|
54
|
+
placeholder="例如:登录按钮、账号输入框"
|
|
55
|
+
@update:model-value="patchStep({ note: String($event) || undefined })"
|
|
56
|
+
/>
|
|
57
|
+
</el-form-item>
|
|
58
|
+
<div class="appium-flow-editor__grid">
|
|
59
|
+
<el-form-item label="节点类型">
|
|
60
|
+
<el-select
|
|
61
|
+
:model-value="defaultKind()"
|
|
62
|
+
:disabled="disabled"
|
|
63
|
+
@update:model-value="patchFlow({ nodeKind: $event as FlowKind })"
|
|
64
|
+
>
|
|
65
|
+
<el-option label="操作" value="action" />
|
|
66
|
+
<el-option label="判断" value="condition" />
|
|
67
|
+
<el-option label="校验" value="assertion" />
|
|
68
|
+
</el-select>
|
|
69
|
+
</el-form-item>
|
|
70
|
+
<el-form-item label="超时时间 ms">
|
|
71
|
+
<el-input-number
|
|
72
|
+
:model-value="step.timeoutMs || undefined"
|
|
73
|
+
:disabled="disabled"
|
|
74
|
+
:min="0"
|
|
75
|
+
:max="999999"
|
|
76
|
+
controls-position="right"
|
|
77
|
+
@update:model-value="patchStep({ timeoutMs: Number($event || 0) || undefined })"
|
|
78
|
+
/>
|
|
79
|
+
</el-form-item>
|
|
80
|
+
</div>
|
|
81
|
+
<el-form-item
|
|
82
|
+
v-if="step.type === 'input' || step.type === 'inputIfExists' || step.type === 'assertText'"
|
|
83
|
+
label="文本内容"
|
|
84
|
+
>
|
|
85
|
+
<el-input
|
|
86
|
+
:model-value="step.value || ''"
|
|
87
|
+
:disabled="disabled"
|
|
88
|
+
@update:model-value="patchStep({ value: String($event) })"
|
|
89
|
+
/>
|
|
90
|
+
</el-form-item>
|
|
91
|
+
<div v-if="step.selector" class="appium-flow-editor__grid">
|
|
92
|
+
<el-form-item label="Selector 类型">
|
|
93
|
+
<el-select
|
|
94
|
+
:model-value="step.selector.strategy"
|
|
95
|
+
:disabled="disabled"
|
|
96
|
+
@update:model-value="patchStep({ selector: { ...step.selector!, strategy: $event as AppiumSelector['strategy'] } })"
|
|
97
|
+
>
|
|
98
|
+
<el-option label="accessibility id" value="accessibilityId" />
|
|
99
|
+
<el-option label="id" value="id" />
|
|
100
|
+
<el-option label="android uiAutomator" value="androidUiAutomator" />
|
|
101
|
+
<el-option label="xpath" value="xpath" />
|
|
102
|
+
</el-select>
|
|
103
|
+
</el-form-item>
|
|
104
|
+
<el-form-item label="Selector 值">
|
|
105
|
+
<el-input
|
|
106
|
+
:model-value="step.selector.value || ''"
|
|
107
|
+
:disabled="disabled"
|
|
108
|
+
@update:model-value="patchStep({ selector: { ...step.selector!, value: String($event) } })"
|
|
109
|
+
/>
|
|
110
|
+
</el-form-item>
|
|
111
|
+
</div>
|
|
112
|
+
<div v-if="step.selector" class="appium-flow-editor__grid">
|
|
113
|
+
<el-form-item label="父级上下文类型">
|
|
114
|
+
<el-select
|
|
115
|
+
:model-value="step.contextSelector?.strategy || ''"
|
|
116
|
+
:disabled="disabled"
|
|
117
|
+
clearable
|
|
118
|
+
placeholder="不使用父级"
|
|
119
|
+
@update:model-value="patchStep({ contextSelector: $event ? { ...(step.contextSelector || { value: '' }), strategy: $event as AppiumSelector['strategy'] } : undefined })"
|
|
120
|
+
>
|
|
121
|
+
<el-option label="accessibility id" value="accessibilityId" />
|
|
122
|
+
<el-option label="id" value="id" />
|
|
123
|
+
<el-option label="android uiAutomator" value="androidUiAutomator" />
|
|
124
|
+
<el-option label="xpath" value="xpath" />
|
|
125
|
+
</el-select>
|
|
126
|
+
</el-form-item>
|
|
127
|
+
<el-form-item label="父级上下文值">
|
|
128
|
+
<el-input
|
|
129
|
+
:model-value="step.contextSelector?.value || ''"
|
|
130
|
+
:disabled="disabled || !step.contextSelector"
|
|
131
|
+
@update:model-value="patchStep({ contextSelector: { ...(step.contextSelector || { strategy: 'xpath' }), value: String($event) } })"
|
|
132
|
+
/>
|
|
133
|
+
</el-form-item>
|
|
134
|
+
</div>
|
|
135
|
+
<el-form-item label="可选步骤">
|
|
136
|
+
<el-switch
|
|
137
|
+
:model-value="Boolean(step.optional)"
|
|
138
|
+
:disabled="disabled"
|
|
139
|
+
active-text="找不到时跳过"
|
|
140
|
+
inactive-text="找不到时失败"
|
|
141
|
+
@update:model-value="patchStep({ optional: Boolean($event) })"
|
|
142
|
+
/>
|
|
143
|
+
</el-form-item>
|
|
144
|
+
<div v-if="defaultKind() === 'condition'" class="appium-flow-editor__grid">
|
|
145
|
+
<el-form-item label="指定文本(可选)">
|
|
146
|
+
<el-input
|
|
147
|
+
:model-value="step.value || ''"
|
|
148
|
+
:disabled="disabled"
|
|
149
|
+
clearable
|
|
150
|
+
placeholder="不填写时仅判断组件是否存在"
|
|
151
|
+
@update:model-value="patchStep({ value: String($event) })"
|
|
152
|
+
/>
|
|
153
|
+
</el-form-item>
|
|
154
|
+
<el-form-item label="文本匹配方式">
|
|
155
|
+
<el-select
|
|
156
|
+
:model-value="step.flow?.textMatch || 'contains'"
|
|
157
|
+
:disabled="disabled || !step.value"
|
|
158
|
+
@update:model-value="patchFlow({ textMatch: $event as 'contains' | 'exact' })"
|
|
159
|
+
>
|
|
160
|
+
<el-option label="模糊匹配(包含)" value="contains" />
|
|
161
|
+
<el-option label="精准匹配(完全一致)" value="exact" />
|
|
162
|
+
</el-select>
|
|
163
|
+
</el-form-item>
|
|
164
|
+
</div>
|
|
165
|
+
<div v-if="defaultKind() !== 'condition'" class="appium-flow-editor__actions">
|
|
166
|
+
<el-button
|
|
167
|
+
:disabled="disabled"
|
|
168
|
+
@click="patchFlow({ nodeKind: 'condition', yesTargetId: '', noTargetId: '' })"
|
|
169
|
+
>
|
|
170
|
+
转为判断节点
|
|
171
|
+
</el-button>
|
|
172
|
+
</div>
|
|
173
|
+
</el-form>
|
|
174
|
+
</div>
|
|
175
|
+
</template>
|