android-midscene-automation 0.1.5 → 0.1.9
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/README.md +8 -10
- package/package.json +3 -3
- package/server/appium-recorder/appium-runner.ts +362 -26
- package/server/appium-recorder/repository.ts +89 -12
- package/src/App.vue +52 -7
- package/src/appium-recorder/AppiumPage.vue +504 -124
- package/src/appium-recorder/components/ComponentTree.vue +14 -1
- package/src/appium-recorder/components/NodeDetail.vue +24 -118
- package/src/appium-recorder/components/RecordedSteps.vue +898 -36
- package/src/appium-recorder/tree.ts +55 -9
- package/src/appium-recorder/types.ts +33 -1
- package/src/style.css +659 -11
|
@@ -12,6 +12,10 @@ export type AppiumRecordedStepRecord = {
|
|
|
12
12
|
type:
|
|
13
13
|
| 'tap'
|
|
14
14
|
| 'input'
|
|
15
|
+
| 'tapIfExists'
|
|
16
|
+
| 'inputIfExists'
|
|
17
|
+
| 'clearIfExists'
|
|
18
|
+
| 'backIfExists'
|
|
15
19
|
| 'waitFor'
|
|
16
20
|
| 'assertExists'
|
|
17
21
|
| 'key'
|
|
@@ -25,23 +29,72 @@ export type AppiumRecordedStepRecord = {
|
|
|
25
29
|
| 'waitDisappear'
|
|
26
30
|
| 'assertText'
|
|
27
31
|
| 'longPress'
|
|
28
|
-
| 'pinch'
|
|
32
|
+
| 'pinch'
|
|
33
|
+
| 'runScript';
|
|
29
34
|
label: string;
|
|
30
35
|
selector?: {
|
|
31
36
|
strategy: 'accessibilityId' | 'id' | 'androidUiAutomator' | 'xpath' | 'bounds';
|
|
32
37
|
value?: string;
|
|
33
38
|
centerX?: number;
|
|
34
39
|
centerY?: number;
|
|
40
|
+
unique?: boolean;
|
|
41
|
+
matchCount?: number;
|
|
42
|
+
source?: 'node' | 'ancestor' | 'fallback';
|
|
35
43
|
};
|
|
44
|
+
contextSelector?: {
|
|
45
|
+
strategy: 'accessibilityId' | 'id' | 'androidUiAutomator' | 'xpath' | 'bounds';
|
|
46
|
+
value?: string;
|
|
47
|
+
centerX?: number;
|
|
48
|
+
centerY?: number;
|
|
49
|
+
unique?: boolean;
|
|
50
|
+
matchCount?: number;
|
|
51
|
+
source?: 'node' | 'ancestor' | 'fallback';
|
|
52
|
+
};
|
|
53
|
+
selectorChain?: Array<{
|
|
54
|
+
strategy: 'accessibilityId' | 'id' | 'androidUiAutomator' | 'xpath' | 'bounds';
|
|
55
|
+
value?: string;
|
|
56
|
+
centerX?: number;
|
|
57
|
+
centerY?: number;
|
|
58
|
+
unique?: boolean;
|
|
59
|
+
matchCount?: number;
|
|
60
|
+
source?: 'node' | 'ancestor' | 'fallback';
|
|
61
|
+
}>;
|
|
36
62
|
fallback?: {
|
|
37
63
|
strategy: 'accessibilityId' | 'id' | 'androidUiAutomator' | 'xpath' | 'bounds';
|
|
38
64
|
value?: string;
|
|
39
65
|
centerX?: number;
|
|
40
66
|
centerY?: number;
|
|
67
|
+
unique?: boolean;
|
|
68
|
+
matchCount?: number;
|
|
69
|
+
source?: 'node' | 'ancestor' | 'fallback';
|
|
41
70
|
};
|
|
42
71
|
value?: string;
|
|
72
|
+
optional?: boolean;
|
|
43
73
|
keyCode?: number;
|
|
44
74
|
timeoutMs?: number;
|
|
75
|
+
flow?: {
|
|
76
|
+
scope?: 'pre' | 'main' | 'post';
|
|
77
|
+
nodeKind?: 'action' | 'condition' | 'assertion';
|
|
78
|
+
yesTargetId?: string;
|
|
79
|
+
noTargetId?: string;
|
|
80
|
+
parentConditionId?: string;
|
|
81
|
+
parentBranch?: 'yes' | 'no';
|
|
82
|
+
successTargetId?: string;
|
|
83
|
+
failureTargetId?: string;
|
|
84
|
+
collapsed?: boolean;
|
|
85
|
+
};
|
|
86
|
+
pageBefore?: {
|
|
87
|
+
activity?: string;
|
|
88
|
+
packageName?: string;
|
|
89
|
+
treeSignature?: string;
|
|
90
|
+
selectedNodePath?: string;
|
|
91
|
+
};
|
|
92
|
+
pageAfter?: {
|
|
93
|
+
activity?: string;
|
|
94
|
+
packageName?: string;
|
|
95
|
+
treeSignature?: string;
|
|
96
|
+
selectedNodePath?: string;
|
|
97
|
+
};
|
|
45
98
|
swipe?: {
|
|
46
99
|
startX: number;
|
|
47
100
|
startY: number;
|
|
@@ -80,16 +133,14 @@ type AppiumRecordedScriptRow = {
|
|
|
80
133
|
app_package: string;
|
|
81
134
|
app_activity: string | null;
|
|
82
135
|
device_id: string | null;
|
|
83
|
-
|
|
136
|
+
flow_json: string | null;
|
|
84
137
|
created_at: string;
|
|
85
138
|
updated_at: string;
|
|
86
139
|
};
|
|
87
140
|
|
|
88
141
|
let initialized = false;
|
|
89
142
|
|
|
90
|
-
function
|
|
91
|
-
if (initialized) return;
|
|
92
|
-
initialized = true;
|
|
143
|
+
function createAppiumRecordedScriptsTable() {
|
|
93
144
|
runSql(`
|
|
94
145
|
CREATE TABLE IF NOT EXISTS appium_recorded_scripts (
|
|
95
146
|
id TEXT PRIMARY KEY,
|
|
@@ -97,7 +148,7 @@ function initDb() {
|
|
|
97
148
|
app_package TEXT NOT NULL,
|
|
98
149
|
app_activity TEXT,
|
|
99
150
|
device_id TEXT,
|
|
100
|
-
|
|
151
|
+
flow_json TEXT NOT NULL DEFAULT '[]',
|
|
101
152
|
created_at TEXT NOT NULL,
|
|
102
153
|
updated_at TEXT NOT NULL
|
|
103
154
|
);
|
|
@@ -106,6 +157,32 @@ function initDb() {
|
|
|
106
157
|
`);
|
|
107
158
|
}
|
|
108
159
|
|
|
160
|
+
function initDb() {
|
|
161
|
+
if (initialized) return;
|
|
162
|
+
initialized = true;
|
|
163
|
+
createAppiumRecordedScriptsTable();
|
|
164
|
+
const columns = querySql<{ name: string }>('PRAGMA table_info(appium_recorded_scripts);').map((column) => column.name);
|
|
165
|
+
if (columns.includes('steps_json') || !columns.includes('flow_json')) {
|
|
166
|
+
runSql('DROP TABLE appium_recorded_scripts;');
|
|
167
|
+
createAppiumRecordedScriptsTable();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseStepList(value?: string | null) {
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(value || '[]');
|
|
174
|
+
if (!Array.isArray(parsed)) return [];
|
|
175
|
+
return parsed.filter((step) => (
|
|
176
|
+
step
|
|
177
|
+
&& typeof step === 'object'
|
|
178
|
+
&& typeof step.id === 'string'
|
|
179
|
+
&& typeof step.type === 'string'
|
|
180
|
+
)) as AppiumRecordedStepRecord[];
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
109
186
|
function rowToRecord(row: AppiumRecordedScriptRow): AppiumRecordedScriptRecord {
|
|
110
187
|
return {
|
|
111
188
|
id: row.id,
|
|
@@ -113,7 +190,7 @@ function rowToRecord(row: AppiumRecordedScriptRow): AppiumRecordedScriptRecord {
|
|
|
113
190
|
appPackage: row.app_package,
|
|
114
191
|
appActivity: row.app_activity || '',
|
|
115
192
|
deviceId: row.device_id || '',
|
|
116
|
-
steps:
|
|
193
|
+
steps: parseStepList(row.flow_json),
|
|
117
194
|
createdAt: row.created_at,
|
|
118
195
|
updatedAt: row.updated_at,
|
|
119
196
|
};
|
|
@@ -128,7 +205,7 @@ export function listAppiumRecordedScripts() {
|
|
|
128
205
|
app_package,
|
|
129
206
|
app_activity,
|
|
130
207
|
device_id,
|
|
131
|
-
|
|
208
|
+
flow_json,
|
|
132
209
|
created_at,
|
|
133
210
|
updated_at
|
|
134
211
|
FROM appium_recorded_scripts
|
|
@@ -145,7 +222,7 @@ export function getAppiumRecordedScript(id: string) {
|
|
|
145
222
|
app_package,
|
|
146
223
|
app_activity,
|
|
147
224
|
device_id,
|
|
148
|
-
|
|
225
|
+
flow_json,
|
|
149
226
|
created_at,
|
|
150
227
|
updated_at
|
|
151
228
|
FROM appium_recorded_scripts
|
|
@@ -180,7 +257,7 @@ export function saveAppiumRecordedScript(input: {
|
|
|
180
257
|
app_package = ${sqlString(appPackage)},
|
|
181
258
|
app_activity = ${sqlNullableString(input.appActivity || '')},
|
|
182
259
|
device_id = ${sqlNullableString(input.deviceId || '')},
|
|
183
|
-
|
|
260
|
+
flow_json = ${sqlJson(input.steps || [])},
|
|
184
261
|
updated_at = ${sqlString(now)}
|
|
185
262
|
WHERE id = ${sqlString(input.id)};
|
|
186
263
|
`);
|
|
@@ -194,7 +271,7 @@ export function saveAppiumRecordedScript(input: {
|
|
|
194
271
|
app_package,
|
|
195
272
|
app_activity,
|
|
196
273
|
device_id,
|
|
197
|
-
|
|
274
|
+
flow_json,
|
|
198
275
|
created_at,
|
|
199
276
|
updated_at
|
|
200
277
|
)
|
|
@@ -212,7 +289,7 @@ export function saveAppiumRecordedScript(input: {
|
|
|
212
289
|
app_package = excluded.app_package,
|
|
213
290
|
app_activity = excluded.app_activity,
|
|
214
291
|
device_id = excluded.device_id,
|
|
215
|
-
|
|
292
|
+
flow_json = excluded.flow_json,
|
|
216
293
|
updated_at = excluded.updated_at;
|
|
217
294
|
`);
|
|
218
295
|
|
package/src/App.vue
CHANGED
|
@@ -72,6 +72,7 @@ const playgroundFrameUrl = ref('');
|
|
|
72
72
|
const playgroundFrameSignature = ref('');
|
|
73
73
|
const devicePreviewUrl = ref('');
|
|
74
74
|
const devicePreviewMode = ref<'stream' | 'screenshot' | ''>('');
|
|
75
|
+
const backendOffline = shallowRef(false);
|
|
75
76
|
const androidDevices = ref<AndroidDevice[]>([]);
|
|
76
77
|
const deviceInterfaceSize = reactive({ width: 0, height: 0 });
|
|
77
78
|
const deviceDebug = reactive({
|
|
@@ -116,6 +117,17 @@ let appiumPreviewTimer: number | null = null;
|
|
|
116
117
|
let executionProgressTimer: number | null = null;
|
|
117
118
|
let scriptRunAbortController: AbortController | null = null;
|
|
118
119
|
|
|
120
|
+
const isBackendFetchError = (error: unknown) => {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error || '');
|
|
122
|
+
return error instanceof TypeError || /Failed to fetch|NetworkError|Load failed/i.test(message);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const stopPlaygroundPollTimer = () => {
|
|
126
|
+
if (!playgroundPollTimer) return;
|
|
127
|
+
window.clearInterval(playgroundPollTimer);
|
|
128
|
+
playgroundPollTimer = null;
|
|
129
|
+
};
|
|
130
|
+
|
|
119
131
|
const form = reactive({
|
|
120
132
|
promptTitle: '',
|
|
121
133
|
testName: '',
|
|
@@ -739,10 +751,20 @@ const testModel = async (key: 'midscene' | 'scriptOptimizer') => {
|
|
|
739
751
|
};
|
|
740
752
|
|
|
741
753
|
const loadAndroidDevices = async () => {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
754
|
+
if (backendOffline.value) return;
|
|
755
|
+
try {
|
|
756
|
+
const payload = await api.getAndroidDevices();
|
|
757
|
+
backendOffline.value = false;
|
|
758
|
+
androidDevices.value = payload.devices || [];
|
|
759
|
+
playgroundDeviceId.value = payload.currentDeviceId || '';
|
|
760
|
+
playgroundAvailable.value = androidDevices.value.some((item) => item.status === 'device');
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (isBackendFetchError(error)) {
|
|
763
|
+
markBackendOffline();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
746
768
|
};
|
|
747
769
|
|
|
748
770
|
const loadDeviceInterface = async () => {
|
|
@@ -757,6 +779,7 @@ const loadDeviceInterface = async () => {
|
|
|
757
779
|
};
|
|
758
780
|
|
|
759
781
|
const loadAdbPreview = () => {
|
|
782
|
+
if (backendOffline.value) return;
|
|
760
783
|
if (!playgroundDeviceId.value) {
|
|
761
784
|
devicePreviewUrl.value = '';
|
|
762
785
|
devicePreviewMode.value = '';
|
|
@@ -786,6 +809,7 @@ const refreshDevicePreview = () => {
|
|
|
786
809
|
};
|
|
787
810
|
|
|
788
811
|
const startAppiumPreviewTimer = () => {
|
|
812
|
+
if (backendOffline.value) return;
|
|
789
813
|
if (appiumPreviewTimer) return;
|
|
790
814
|
loadAdbPreview();
|
|
791
815
|
appiumPreviewTimer = window.setInterval(loadAdbPreview, 1000);
|
|
@@ -797,7 +821,22 @@ const stopAppiumPreviewTimer = () => {
|
|
|
797
821
|
appiumPreviewTimer = null;
|
|
798
822
|
};
|
|
799
823
|
|
|
824
|
+
const markBackendOffline = () => {
|
|
825
|
+
if (backendOffline.value) return;
|
|
826
|
+
backendOffline.value = true;
|
|
827
|
+
playgroundAvailable.value = false;
|
|
828
|
+
playgroundFrameUrl.value = '';
|
|
829
|
+
playgroundFrameSignature.value = '';
|
|
830
|
+
devicePreviewUrl.value = '';
|
|
831
|
+
devicePreviewMode.value = '';
|
|
832
|
+
playgroundPreviewError.value = '本地服务已断开,请重新运行启动命令后刷新页面。';
|
|
833
|
+
errorMessage.value = playgroundPreviewError.value;
|
|
834
|
+
stopPlaygroundPollTimer();
|
|
835
|
+
stopAppiumPreviewTimer();
|
|
836
|
+
};
|
|
837
|
+
|
|
800
838
|
const loadPlaygroundStatus = async () => {
|
|
839
|
+
if (backendOffline.value) return;
|
|
801
840
|
if (!playgroundDeviceId.value) {
|
|
802
841
|
playgroundAvailable.value = false;
|
|
803
842
|
playgroundPreviewError.value = '';
|
|
@@ -844,6 +883,10 @@ const loadPlaygroundStatus = async () => {
|
|
|
844
883
|
loadAdbPreview();
|
|
845
884
|
}
|
|
846
885
|
} catch (error) {
|
|
886
|
+
if (isBackendFetchError(error)) {
|
|
887
|
+
markBackendOffline();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
847
890
|
playgroundAvailable.value = false;
|
|
848
891
|
playgroundFrameUrl.value = '';
|
|
849
892
|
playgroundFrameSignature.value = '';
|
|
@@ -1207,15 +1250,17 @@ onMounted(async () => {
|
|
|
1207
1250
|
await Promise.allSettled([loadConfig(), loadAppPresets(), loadAndroidDevices(), loadSavedScripts(), loadModelUsageRecords()]);
|
|
1208
1251
|
await loadPlaygroundStatus();
|
|
1209
1252
|
playgroundPollTimer = window.setInterval(() => {
|
|
1253
|
+
if (backendOffline.value) {
|
|
1254
|
+
stopPlaygroundPollTimer();
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1210
1257
|
void loadAndroidDevices();
|
|
1211
1258
|
void loadPlaygroundStatus();
|
|
1212
1259
|
}, 2500);
|
|
1213
1260
|
});
|
|
1214
1261
|
|
|
1215
1262
|
onUnmounted(() => {
|
|
1216
|
-
|
|
1217
|
-
window.clearInterval(playgroundPollTimer);
|
|
1218
|
-
}
|
|
1263
|
+
stopPlaygroundPollTimer();
|
|
1219
1264
|
stopAppiumPreviewTimer();
|
|
1220
1265
|
stopExecutionProgressTimer();
|
|
1221
1266
|
scriptRunAbortController?.abort();
|