testdriverai 4.2.19 → 5.0.0-beta-8
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/.github/dependabot.yml +1 -1
- package/.github/workflows/test_interp.yml +1 -1
- package/.github/workflows/testdriver.yml +28 -0
- package/README.md +3 -2
- package/agent.js +90 -24
- package/electron/overlay.html +39 -5
- package/electron/overlay.js +57 -24
- package/electron/td.png +0 -0
- package/index.js +2 -2
- package/lib/commander.js +10 -2
- package/lib/commands.js +94 -50
- package/lib/config.js +4 -2
- package/lib/events.js +4 -0
- package/lib/focus-application.js +16 -0
- package/lib/generator.js +16 -0
- package/lib/init.js +59 -21
- package/lib/keymaps/sandbox.js +124 -0
- package/lib/logger.js +4 -1
- package/lib/overlay.js +0 -2
- package/lib/redraw.js +3 -1
- package/lib/sandbox.js +64 -0
- package/lib/speak.js +3 -0
- package/lib/system.js +69 -24
- package/lib/websockets.js +78 -0
- package/package-lock.json +8705 -0
- package/package.json +8 -6
- package/test.md +8 -0
- /package/lib/{keymap.js → keymaps/robot.js} +0 -0
package/lib/sandbox.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const WebSocket = require('ws');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
|
|
5
|
+
class Sandbox {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.socket = null;
|
|
8
|
+
this.ps = {};
|
|
9
|
+
this.heartbeat = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
send(message) {
|
|
13
|
+
let resolvePromise;
|
|
14
|
+
if (this.socket) {
|
|
15
|
+
message.requestId = Math.random().toString(36).substring(7) + new Date().getTime();
|
|
16
|
+
let p = new Promise((resolve, reject) => {
|
|
17
|
+
this.socket.send(JSON.stringify(message));
|
|
18
|
+
resolvePromise = resolve;
|
|
19
|
+
});
|
|
20
|
+
this.ps[message.requestId] = {promise: p, resolve: resolvePromise};
|
|
21
|
+
return p;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async boot() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
|
|
28
|
+
this.socket = new WebSocket(`${config.TD_API_ROOT.replace('https://', 'wss://')}`);
|
|
29
|
+
|
|
30
|
+
// handle errors
|
|
31
|
+
this.socket.on('error', (err) => {
|
|
32
|
+
console.log('Socket Closed');
|
|
33
|
+
console.log('If this happens during setup, check your API Key (`TD_API_KEY` in `.env`).');
|
|
34
|
+
err && console.log(err);
|
|
35
|
+
clearInterval(this.heartbeat);
|
|
36
|
+
reject();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.socket.on('open', async () => {
|
|
40
|
+
|
|
41
|
+
this.heartbeat = setInterval(() => {
|
|
42
|
+
this.send({type: 'ping'});
|
|
43
|
+
}, 5000);
|
|
44
|
+
|
|
45
|
+
resolve(this);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.socket.on('message', (raw) => {
|
|
49
|
+
let message = JSON.parse(raw);
|
|
50
|
+
|
|
51
|
+
if (this.ps[message.requestId]) {
|
|
52
|
+
this.ps[message.requestId].resolve(message);
|
|
53
|
+
delete this.ps[message.requestId];
|
|
54
|
+
} else {
|
|
55
|
+
console.log('unhandled message', message);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sandboxInstance = new Sandbox();
|
|
64
|
+
module.exports = sandboxInstance;
|
package/lib/speak.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
const say = require("say");
|
|
2
2
|
const config = require("./config");
|
|
3
|
+
const websockets = require("./websockets");
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
module.exports = (message) => {
|
|
5
7
|
if (config["TD_SPEAK"]) {
|
|
6
8
|
say.stop();
|
|
9
|
+
// websockets.sendToClients("output", message);
|
|
7
10
|
if (process.platform === "darwin") {
|
|
8
11
|
say.speak(message, "Fred", 1.2);
|
|
9
12
|
} else {
|
package/lib/system.js
CHANGED
|
@@ -2,13 +2,28 @@
|
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const os = require("os");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const screenshot = require("screenshot-desktop");
|
|
6
5
|
const si = require("systeminformation");
|
|
7
6
|
const robot = require("robotjs");
|
|
8
7
|
const sharp = require("sharp");
|
|
9
8
|
const { emitter, events } = require("./events.js");
|
|
9
|
+
const sandbox = require("./sandbox.js");
|
|
10
|
+
const config = require("./config.js");
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
let scshotdesk;
|
|
13
|
+
if (!config.TD_VM) {
|
|
14
|
+
scshotdesk = require("screenshot-desktop");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const screenshot = async (options) => {
|
|
18
|
+
|
|
19
|
+
if (config.TD_VM) {
|
|
20
|
+
let {base64} = await sandbox.send({type: 'screenshot'});
|
|
21
|
+
let image = Buffer.from(base64, 'base64');
|
|
22
|
+
fs.writeFileSync(options.filename, image);
|
|
23
|
+
} else {
|
|
24
|
+
return await scshotdesk({ filename: options.filename, format: "png" });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
12
27
|
|
|
13
28
|
let primaryDisplay = null;
|
|
14
29
|
|
|
@@ -26,7 +41,13 @@ const getPrimaryDisplay = async () => {
|
|
|
26
41
|
};
|
|
27
42
|
|
|
28
43
|
const getSystemInformationOsInfo = async () => {
|
|
29
|
-
|
|
44
|
+
if (config.TD_VM) {
|
|
45
|
+
return {
|
|
46
|
+
os: 'linux'
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
return await si.osInfo();
|
|
50
|
+
}
|
|
30
51
|
};
|
|
31
52
|
|
|
32
53
|
let countImages = 0;
|
|
@@ -37,7 +58,7 @@ const tmpFilename = () => {
|
|
|
37
58
|
|
|
38
59
|
const captureAndResize = async (scale = 1, silent = false, mouse = false) => {
|
|
39
60
|
try {
|
|
40
|
-
|
|
61
|
+
|
|
41
62
|
if (!silent) {
|
|
42
63
|
emitter.emit(events.screenCapture.start, {
|
|
43
64
|
scale,
|
|
@@ -51,21 +72,35 @@ const captureAndResize = async (scale = 1, silent = false, mouse = false) => {
|
|
|
51
72
|
|
|
52
73
|
await screenshot({ filename: step1, format: "png" });
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
let sharpInstance;
|
|
76
|
+
if (config.TD_VM) {
|
|
77
|
+
|
|
78
|
+
// resize to 1:1 px ratio
|
|
79
|
+
sharpInstance = sharp(step1).resize(
|
|
80
|
+
Math.floor(1024 * scale),
|
|
81
|
+
Math.floor(768 * scale),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
} else {
|
|
56
85
|
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
const primaryDisplay = await getPrimaryDisplay();
|
|
87
|
+
|
|
88
|
+
sharpInstance = sharp(step1).resize(
|
|
89
|
+
Math.floor(primaryDisplay.currentResX * scale),
|
|
90
|
+
Math.floor(primaryDisplay.currentResY * scale),
|
|
91
|
+
);
|
|
59
92
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
// Fetch the mouse position
|
|
94
|
+
const mousePos = robot.getMousePos();
|
|
95
|
+
|
|
96
|
+
// Location of cursor image
|
|
97
|
+
const cursorPath = path.join(__dirname, "resources", "cursor.png");
|
|
98
|
+
|
|
99
|
+
if (mouse) {
|
|
100
|
+
// composite the mouse image ontop
|
|
101
|
+
sharpInstance.composite([{ input: cursorPath, left: mousePos.x, top: mousePos.y }]);
|
|
102
|
+
}
|
|
65
103
|
|
|
66
|
-
if (mouse) {
|
|
67
|
-
// composite the mouse image ontop
|
|
68
|
-
sharpInstance.composite([{ input: cursorPath, left: mousePos.x, top: mousePos.y }]);
|
|
69
104
|
}
|
|
70
105
|
|
|
71
106
|
await sharpInstance.toFile(step2);
|
|
@@ -115,7 +150,7 @@ const platform = () => {
|
|
|
115
150
|
// Import get-windows using dynamic import for ES module compatibility
|
|
116
151
|
let activeWindowFn = null;
|
|
117
152
|
const initializeActiveWindow = async () => {
|
|
118
|
-
if (!activeWindowFn) {
|
|
153
|
+
if (!activeWindowFn && !config.TD_VM) {
|
|
119
154
|
const { activeWindow } = await import('get-windows');
|
|
120
155
|
activeWindowFn = activeWindow;
|
|
121
156
|
}
|
|
@@ -124,17 +159,27 @@ const initializeActiveWindow = async () => {
|
|
|
124
159
|
|
|
125
160
|
// this is the focused window
|
|
126
161
|
const activeWin = async () => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
162
|
+
|
|
163
|
+
if (config.TD_VM) {
|
|
164
|
+
|
|
165
|
+
return "error getting active window, proceed normally";
|
|
166
|
+
|
|
167
|
+
} else {
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const activeWindow = await initializeActiveWindow();
|
|
171
|
+
return await activeWindow();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logger.error('Error getting active window: %s', error);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
133
177
|
}
|
|
134
178
|
};
|
|
135
179
|
|
|
136
180
|
const getMousePosition = async () => {
|
|
137
|
-
|
|
181
|
+
// @todo vm does expose mouse position, add it back
|
|
182
|
+
return config.TD_VM ? await robot.getMousePos() : null;
|
|
138
183
|
};
|
|
139
184
|
|
|
140
185
|
module.exports = {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
|
|
3
|
+
class WebSocketServerSingleton {
|
|
4
|
+
constructor() {
|
|
5
|
+
if (!WebSocketServerSingleton.instance) {
|
|
6
|
+
this.wss = new WebSocket.Server({ port: 8080 }, () => {
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
this.clients = new Set();
|
|
10
|
+
this.eventListeners = new Map();
|
|
11
|
+
|
|
12
|
+
this.wss.on('error', (error) => {
|
|
13
|
+
console.error('WebSocket server error:', error);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
this.wss.on('connection', (ws) => {
|
|
17
|
+
|
|
18
|
+
console.log('Client connected');
|
|
19
|
+
this.clients.add(ws);
|
|
20
|
+
|
|
21
|
+
ws.on('message', (message) => {
|
|
22
|
+
try {
|
|
23
|
+
const parsedMessage = JSON.parse(message);
|
|
24
|
+
if (parsedMessage.event) {
|
|
25
|
+
this.triggerEvent(parsedMessage.event, parsedMessage);
|
|
26
|
+
} else {
|
|
27
|
+
console.error('Parsed message does not contain an event property');
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Error parsing message:', error);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
ws.on('close', () => {
|
|
35
|
+
this.clients.delete(ws);
|
|
36
|
+
console.log('Client disconnected');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
ws.on('error', (error) => {
|
|
40
|
+
console.error('WebSocket error:', error);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
WebSocketServerSingleton.instance = this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return WebSocketServerSingleton.instance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
addEventListener(event, listener) {
|
|
51
|
+
if (!this.eventListeners.has(event)) {
|
|
52
|
+
this.eventListeners.set(event, []);
|
|
53
|
+
}
|
|
54
|
+
this.eventListeners.get(event).push(listener);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
triggerEvent(event, data) {
|
|
58
|
+
if (this.eventListeners.has(event)) {
|
|
59
|
+
for (const listener of this.eventListeners.get(event)) {
|
|
60
|
+
listener(data);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sendToClients(event, message) {
|
|
66
|
+
const data = JSON.stringify({ event, message });
|
|
67
|
+
for (const client of this.clients) {
|
|
68
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
69
|
+
client.send(data);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const instance = new WebSocketServerSingleton();
|
|
76
|
+
Object.freeze(instance);
|
|
77
|
+
|
|
78
|
+
module.exports = instance;
|