Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/core/lib/CommandFormatter.ts
1029 views
1
import ICommandMeta from '@secret-agent/interfaces/ICommandMeta';
2
import { IInteractionGroup } from '@secret-agent/interfaces/IInteractions';
3
import { getKeyboardKey } from '@secret-agent/interfaces/IKeyboardLayoutUS';
4
import { getNodePointerFnName } from '@secret-agent/interfaces/jsPathFnNames';
5
import TypeSerializer from '@secret-agent/commons/TypeSerializer';
6
import ICommandWithResult from '../interfaces/ICommandWithResult';
7
8
export default class CommandFormatter {
9
public static toString(command: ICommandMeta) {
10
if (!command.args) {
11
return `${command.name}()`;
12
}
13
const args = JSON.parse(command.args).filter(x => x !== null);
14
if (command.name === 'execJsPath') {
15
return formatJsPath(args[0]);
16
}
17
if (command.name === 'interact') {
18
const interacts = args.map((x: IInteractionGroup) => {
19
return x
20
.map(y => {
21
const extras: any = {};
22
for (const [key, value] of Object.entries(y)) {
23
if (
24
key === 'mouseButton' ||
25
key === 'keyboardDelayBetween' ||
26
key === 'delayMillis'
27
) {
28
extras[key] = value;
29
}
30
}
31
let pathString = '';
32
const path = y.mousePosition ?? y.delayElement ?? y.delayNode;
33
if (path) {
34
// mouse path
35
if (path.length === 2 && typeof path[0] === 'number' && typeof path[1] === 'number') {
36
pathString = path.join(',');
37
} else {
38
pathString = formatJsPath(path);
39
}
40
} else if (y.keyboardCommands) {
41
pathString = y.keyboardCommands
42
.map(keys => {
43
const [keyCommand] = Object.keys(keys);
44
if (keyCommand === 'string') return `"${keys[keyCommand]}"`;
45
46
const keyChar = getKeyboardKey(keys[keyCommand]);
47
if (keyCommand === 'keyPress') return `press: '${keyChar}'`;
48
if (keyCommand === 'up') return `up: '${keyChar}'`;
49
if (keyCommand === 'down') return `down: '${keyChar}'`;
50
return '';
51
})
52
.join(', ');
53
}
54
55
const extrasString = Object.keys(extras).length
56
? `, ${JSON.stringify(extras, null, 2)}`
57
: '';
58
return `${y.command}( ${pathString}${extrasString} )`;
59
})
60
.join(', ');
61
});
62
return interacts.join(';\n');
63
}
64
65
if (command.name === 'detachTab') {
66
const { url } = args[0];
67
return `detachTab(${url})`;
68
}
69
70
if (command.name === 'waitForElement') {
71
return `waitForElement( ${formatJsPath(args[0])} )`;
72
}
73
74
return `${command.name}(${args.map(JSON.stringify).join(', ')})`;
75
}
76
77
public static parseResult(meta: ICommandMeta): ICommandWithResult {
78
const duration = meta.endDate
79
? new Date(meta.endDate).getTime() -
80
new Date(meta.clientStartDate ?? meta.runStartDate).getTime()
81
: null;
82
83
const command: ICommandWithResult = {
84
...meta,
85
label: CommandFormatter.toString(meta),
86
duration,
87
startDate: meta.clientStartDate ?? meta.runStartDate,
88
isError: false,
89
result: undefined,
90
};
91
92
if (meta.result && meta.name === 'takeScreenshot') {
93
const result = JSON.parse(meta.result);
94
const imageType = command.label.includes('jpeg') ? 'jpeg' : 'png';
95
const base64 = result.__type === 'Buffer64' ? result.value : result.data;
96
command.result = `data:image/${imageType}; base64,${base64}`;
97
command.resultType = 'image';
98
} else if (meta.result && meta.resultType?.toLowerCase().includes('error')) {
99
const result = TypeSerializer.parse(meta.result);
100
101
command.isError = true;
102
command.result = result.message;
103
if (result.pathState) {
104
const { step, index } = result.pathState;
105
command.failedJsPathStepIndex = index;
106
command.failedJsPathStep = Array.isArray(step)
107
? `${step[0]}(${step.slice(1).map(x => JSON.stringify(x))})`
108
: step;
109
}
110
} else if (meta.resultType && meta.result) {
111
const result = TypeSerializer.parse(meta.result);
112
command.result = result;
113
if (meta.resultType === 'Object' && result.value) {
114
const resultType = typeof result.value;
115
if (
116
resultType === 'string' ||
117
resultType === 'number' ||
118
resultType === 'boolean' ||
119
resultType === 'undefined'
120
) {
121
command.result = result.value;
122
}
123
124
if (result.nodePointer) {
125
command.resultNodeIds = [result.nodePointer.id];
126
command.resultNodeType = result.nodePointer.type;
127
if (result.nodePointer.iterableItems) {
128
command.result = result.nodePointer.iterableItems;
129
}
130
if (result.nodePointer.iterableIsState) {
131
command.resultNodeIds = result.nodePointer.iterableItems.map(x => x.id);
132
}
133
}
134
}
135
}
136
137
if (!command.resultNodeIds && command.name === 'execJsPath') {
138
const [jsPath] = JSON.parse(command.args);
139
if (typeof jsPath[0] === 'number') command.resultNodeIds = [jsPath[0]];
140
}
141
142
if (!command.resultNodeIds && command.name === 'interact') {
143
const args = JSON.parse(command.args);
144
const mouseInteraction = args.find((x: IInteractionGroup) => {
145
return x.length && x[0].mousePosition && x[0].mousePosition.length === 1;
146
});
147
if (mouseInteraction) {
148
command.resultNodeIds = mouseInteraction.mousePosition;
149
}
150
}
151
152
if (command.result && command.name === 'detachTab') {
153
command.result.prefetchedJsPaths = undefined;
154
}
155
156
if (command.result && command.name.startsWith('go')) {
157
command.result = undefined;
158
}
159
160
// we have shell objects occasionally coming back. hide from ui
161
if (meta.args?.includes(getNodePointerFnName)) {
162
command.result = undefined;
163
}
164
return command;
165
}
166
}
167
168
export function formatJsPath(path: any) {
169
const jsPath = (path ?? [])
170
.map((x, i) => {
171
if (i === 0 && typeof x === 'number') {
172
return `getNodeById(${x})`;
173
}
174
if (Array.isArray(x)) {
175
if (x[0] === getNodePointerFnName) return;
176
return `${x[0]}(${x.slice(1).map(y => JSON.stringify(y))})`;
177
}
178
return x;
179
})
180
.filter(Boolean);
181
182
if (!jsPath.length) return `${path.map(JSON.stringify)}`;
183
184
return `${jsPath.join('.')}`;
185
}
186
187