testdriverai 4.0.80 → 4.1.1
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 +4 -0
- package/electron/overlay.html +302 -0
- package/electron/overlay.js +58 -0
- package/electron/terminal/xterm-fit.js +2 -0
- package/electron/terminal/xterm.css +209 -0
- package/electron/terminal/xterm.js +2 -0
- package/eslint.config.js +1 -1
- package/index.js +58 -20
- package/lib/commander.js +3 -0
- package/lib/commands.js +80 -44
- package/lib/config.js +6 -6
- package/lib/events.js +38 -0
- package/lib/overlay.js +36 -0
- package/lib/redraw.js +25 -27
- package/lib/sdk.js +56 -12
- package/lib/system.js +56 -32
- package/main.js +37 -0
- package/package.json +8 -6
package/lib/redraw.js
CHANGED
|
@@ -6,6 +6,10 @@ const { compare } = require("odiff-bin");
|
|
|
6
6
|
// network
|
|
7
7
|
const si = require('systeminformation');
|
|
8
8
|
const chalk = require('chalk');
|
|
9
|
+
|
|
10
|
+
const networkCooldownMs = 2000;
|
|
11
|
+
const redrawThresholdPercent = 3;
|
|
12
|
+
|
|
9
13
|
let lastTxBytes = null;
|
|
10
14
|
let lastRxBytes = null;
|
|
11
15
|
let measurements = [];
|
|
@@ -20,6 +24,7 @@ async function resetState() {
|
|
|
20
24
|
measurements = [];
|
|
21
25
|
networkSettled = true;
|
|
22
26
|
lastUnsettled = null;
|
|
27
|
+
screenHasRedrawn = false;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
async function updateNetwork() {
|
|
@@ -50,7 +55,7 @@ async function updateNetwork() {
|
|
|
50
55
|
|
|
51
56
|
// log time since unsettlement
|
|
52
57
|
|
|
53
|
-
if ((new Date().getTime() - lastUnsettled) <
|
|
58
|
+
if ((new Date().getTime() - lastUnsettled) < networkCooldownMs) {
|
|
54
59
|
networkSettled = false;
|
|
55
60
|
} else {
|
|
56
61
|
|
|
@@ -77,7 +82,7 @@ async function updateNetwork() {
|
|
|
77
82
|
});
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
async function
|
|
85
|
+
async function imageDiffPercent(image1Url, image2Url) {
|
|
81
86
|
|
|
82
87
|
// generate a temporary file path
|
|
83
88
|
const tmpImage = path.join(os.tmpdir(), `tmp-${Date.now()}.png`);
|
|
@@ -96,11 +101,7 @@ async function imageIsDifferent(image1Url, image2Url) {
|
|
|
96
101
|
return false;
|
|
97
102
|
} else {
|
|
98
103
|
if (reason === "pixel-diff") {
|
|
99
|
-
|
|
100
|
-
return true;
|
|
101
|
-
} else {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
+
return diffPercentage.toFixed(1);
|
|
104
105
|
} else {
|
|
105
106
|
return false;
|
|
106
107
|
}
|
|
@@ -113,37 +114,33 @@ async function start() {
|
|
|
113
114
|
resetState();
|
|
114
115
|
watchNetwork = setInterval(updateNetwork, 500);
|
|
115
116
|
startImage = await captureScreenPNG();
|
|
117
|
+
startImage = await captureScreenPNG(1, true);
|
|
116
118
|
return startImage;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
async function checkCondition(resolve, startTime, timeoutMs) {
|
|
120
|
-
|
|
121
|
-
let nowImage = await captureScreenPNG();
|
|
122
|
+
let nowImage = await captureScreenPNG(1, true);
|
|
122
123
|
let timeElapsed = Date.now() - startTime;
|
|
124
|
+
let diffPercent = 0;
|
|
125
|
+
let isTimeout = timeElapsed > timeoutMs;
|
|
123
126
|
|
|
124
127
|
if (!screenHasRedrawn) {
|
|
125
|
-
|
|
128
|
+
diffPercent = await imageDiffPercent(startImage, nowImage);
|
|
129
|
+
screenHasRedrawn = diffPercent > redrawThresholdPercent;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
clearInterval(watchNetwork);
|
|
133
|
-
resolve("Timeout reached");
|
|
134
|
-
} else {
|
|
132
|
+
// // log redraw as output
|
|
133
|
+
let redrawText = screenHasRedrawn ? chalk.green(`✓`) : chalk.dim(`${diffPercent}/${redrawThresholdPercent}%`);
|
|
134
|
+
let networkText = networkSettled ? chalk.green(`✓`) : chalk.dim(`${Math.floor((Date.now() - lastUnsettled) / 1000)}/${Math.floor(networkCooldownMs/1000)}s`);
|
|
135
|
+
let timeoutText = isTimeout ? chalk.green(`✓`) : chalk.dim(`${Math.floor((timeElapsed)/1000)}/${(timeoutMs / 1000)}s`);
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!screenHasRedrawn) {
|
|
139
|
-
console.log(chalk.dim(` waiting for screen redraw...`));
|
|
140
|
-
}
|
|
141
|
-
if (!networkSettled) {
|
|
142
|
-
console.log(chalk.dim(` waiting for network to settle...`));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
}
|
|
137
|
+
console.log(` `, chalk.dim('redraw='), redrawText, chalk.dim('network='), networkText, chalk.dim('timeout='), timeoutText);
|
|
146
138
|
|
|
139
|
+
if ((screenHasRedrawn && networkSettled) || isTimeout) {
|
|
140
|
+
clearInterval(watchNetwork);
|
|
141
|
+
console.log('')
|
|
142
|
+
resolve("true");
|
|
143
|
+
} else {
|
|
147
144
|
setTimeout(() => {
|
|
148
145
|
checkCondition(resolve, startTime, timeoutMs);
|
|
149
146
|
}, 1000);
|
|
@@ -151,6 +148,7 @@ async function checkCondition(resolve, startTime, timeoutMs) {
|
|
|
151
148
|
}
|
|
152
149
|
|
|
153
150
|
function wait(timeoutMs) {
|
|
151
|
+
console.log("")
|
|
154
152
|
return new Promise((resolve) => {
|
|
155
153
|
const startTime = Date.now();
|
|
156
154
|
checkCondition(resolve, startTime, timeoutMs);
|
package/lib/sdk.js
CHANGED
|
@@ -5,7 +5,6 @@ const package = require("../package.json");
|
|
|
5
5
|
const version = package.version;
|
|
6
6
|
|
|
7
7
|
const root = config["TD_API_ROOT"];
|
|
8
|
-
const shouldStream = config["TD_STREAM_RESPONSES"];
|
|
9
8
|
|
|
10
9
|
// let token = null;
|
|
11
10
|
|
|
@@ -13,7 +12,7 @@ const outputError = async (error) => {
|
|
|
13
12
|
if (error instanceof Response) {
|
|
14
13
|
console.log(chalk.red(error.status), chalk.red(error.statusText));
|
|
15
14
|
await parseBody(error)
|
|
16
|
-
.then((body) => console.log(chalk.red(body)))
|
|
15
|
+
.then((body) => console.log(chalk.red(JSON.stringify(body, null, 2))))
|
|
17
16
|
.catch(() => {});
|
|
18
17
|
} else {
|
|
19
18
|
console.error("Error:", error);
|
|
@@ -27,13 +26,37 @@ const parseBody = async (response, body) => {
|
|
|
27
26
|
if (!contentType.includes("json") && !contentType.includes("text")) {
|
|
28
27
|
return await response.arrayBuffer();
|
|
29
28
|
}
|
|
29
|
+
body = await response.text();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof body === "string") {
|
|
33
|
+
if (contentType.includes("jsonl")) {
|
|
34
|
+
const result = body
|
|
35
|
+
.split("\n")
|
|
36
|
+
.filter((line) => line.trim().length)
|
|
37
|
+
.map((line) => JSON.parse(line))
|
|
38
|
+
.reduce((result, { type, data }) => {
|
|
39
|
+
if (result[type]) {
|
|
40
|
+
if (typeof result[type] === "string") {
|
|
41
|
+
result[type] += data;
|
|
42
|
+
} else {
|
|
43
|
+
result[type].push(data);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
result[type] = typeof data === "string" ? data : [data];
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}, {});
|
|
50
|
+
for (const key of Object.keys(result)) {
|
|
51
|
+
if (Array.isArray(result[key]) && result[key].length === 1) {
|
|
52
|
+
result[key] = result[key][0];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
30
57
|
if (contentType.includes("json")) {
|
|
31
|
-
return
|
|
58
|
+
return JSON.parse(body);
|
|
32
59
|
}
|
|
33
|
-
return await response.text();
|
|
34
|
-
}
|
|
35
|
-
if (typeof body === "string" && contentType.includes("json")) {
|
|
36
|
-
return JSON.parse(body);
|
|
37
60
|
}
|
|
38
61
|
return body;
|
|
39
62
|
} catch (err) {
|
|
@@ -99,10 +122,12 @@ const req = async (path, data, onChunk) => {
|
|
|
99
122
|
if (response.status >= 300) {
|
|
100
123
|
throw response;
|
|
101
124
|
}
|
|
102
|
-
|
|
125
|
+
const contentType = response.headers.get("Content-Type")?.toLowerCase();
|
|
126
|
+
const isJsonl = contentType === "application/jsonl";
|
|
103
127
|
let result;
|
|
104
128
|
if (onChunk) {
|
|
105
129
|
result = "";
|
|
130
|
+
let lastLineIndex = -1;
|
|
106
131
|
const reader = response.body.getReader();
|
|
107
132
|
while (true) {
|
|
108
133
|
const { done, value } = await reader.read();
|
|
@@ -110,14 +135,33 @@ const req = async (path, data, onChunk) => {
|
|
|
110
135
|
break;
|
|
111
136
|
}
|
|
112
137
|
|
|
113
|
-
|
|
138
|
+
let chunk = new TextDecoder().decode(value);
|
|
139
|
+
|
|
114
140
|
result += chunk;
|
|
115
|
-
|
|
141
|
+
let events = [chunk];
|
|
142
|
+
if (isJsonl) {
|
|
143
|
+
const lines = result.split("\n");
|
|
144
|
+
events = lines
|
|
145
|
+
.slice(lastLineIndex + 1, lines.length - 1)
|
|
146
|
+
.filter((line) => line.length)
|
|
147
|
+
.map((line) => JSON.parse(line));
|
|
148
|
+
|
|
149
|
+
lastLineIndex = lines.length - 2;
|
|
150
|
+
}
|
|
151
|
+
for (const chunk of events) {
|
|
116
152
|
await onChunk(chunk);
|
|
117
153
|
}
|
|
118
154
|
}
|
|
119
|
-
|
|
120
|
-
|
|
155
|
+
|
|
156
|
+
if (isJsonl) {
|
|
157
|
+
const events = result
|
|
158
|
+
.split("\n")
|
|
159
|
+
.slice(lastLineIndex + 1)
|
|
160
|
+
.filter((line) => line.length)
|
|
161
|
+
.map((line) => JSON.parse(line));
|
|
162
|
+
for (const event of events) {
|
|
163
|
+
await onChunk(event);
|
|
164
|
+
}
|
|
121
165
|
}
|
|
122
166
|
}
|
|
123
167
|
|
package/lib/system.js
CHANGED
|
@@ -7,6 +7,7 @@ const si = require("systeminformation");
|
|
|
7
7
|
const activeWindow = require("active-win");
|
|
8
8
|
const robot = require("robotjs");
|
|
9
9
|
const sharp = require("sharp");
|
|
10
|
+
const { emitter, events } = require("./events.js");
|
|
10
11
|
|
|
11
12
|
let primaryDisplay = null;
|
|
12
13
|
|
|
@@ -31,45 +32,67 @@ const tmpFilename = () => {
|
|
|
31
32
|
return path.join(os.tmpdir(), `${new Date().getTime() + Math.random()}.png`);
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
const captureAndResize = async (scale = 1) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
const captureAndResize = async (scale = 1, silent = false) => {
|
|
36
|
+
try {
|
|
37
|
+
const primaryDisplay = await getPrimaryDisplay();
|
|
38
|
+
if (!silent) {
|
|
39
|
+
emitter.emit(events.screenCapture.start, {
|
|
40
|
+
scale,
|
|
41
|
+
display: primaryDisplay,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let step1 = tmpFilename();
|
|
46
|
+
let step2 = tmpFilename();
|
|
47
|
+
|
|
48
|
+
if (process.env["DEV"]) {
|
|
49
|
+
console.log(step2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await screenshot({ filename: step1, format: "png" });
|
|
53
|
+
|
|
54
|
+
// Fetch the mouse position
|
|
55
|
+
const mousePos = robot.getMousePos();
|
|
56
|
+
|
|
57
|
+
// Location of cursor image
|
|
58
|
+
const cursorPath = path.join(__dirname, "resources", "cursor.png");
|
|
59
|
+
|
|
60
|
+
// resize to 1:1 px ratio
|
|
61
|
+
await sharp(step1)
|
|
62
|
+
.resize(
|
|
63
|
+
Math.floor(primaryDisplay.currentResX * scale),
|
|
64
|
+
Math.floor(primaryDisplay.currentResY * scale),
|
|
65
|
+
)
|
|
66
|
+
// composite the mouse image ontop
|
|
67
|
+
.composite([{ input: cursorPath, left: mousePos.x, top: mousePos.y }])
|
|
68
|
+
.toFile(step2);
|
|
69
|
+
if (!silent) {
|
|
70
|
+
emitter.emit(events.screenCapture.end, {
|
|
71
|
+
scale,
|
|
72
|
+
display: primaryDisplay,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return step2;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (!silent) {
|
|
78
|
+
emitter.emit(events.screenCapture.error, {
|
|
79
|
+
error,
|
|
80
|
+
scale,
|
|
81
|
+
display: primaryDisplay,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
42
85
|
}
|
|
43
|
-
|
|
44
|
-
await screenshot({ filename: step1, format: "png" });
|
|
45
|
-
|
|
46
|
-
// Fetch the mouse position
|
|
47
|
-
const mousePos = robot.getMousePos();
|
|
48
|
-
|
|
49
|
-
// Location of cursor image
|
|
50
|
-
const cursorPath = path.join(__dirname, "resources", "cursor.png");
|
|
51
|
-
|
|
52
|
-
// resize to 1:1 px ratio
|
|
53
|
-
await sharp(step1)
|
|
54
|
-
.resize(
|
|
55
|
-
Math.floor(primaryDisplay.currentResX * scale),
|
|
56
|
-
Math.floor(primaryDisplay.currentResY * scale),
|
|
57
|
-
)
|
|
58
|
-
// composite the mouse image ontop
|
|
59
|
-
.composite([{ input: cursorPath, left: mousePos.x, top: mousePos.y}])
|
|
60
|
-
.toFile(step2);
|
|
61
|
-
|
|
62
|
-
return step2;
|
|
63
86
|
};
|
|
64
87
|
|
|
65
88
|
// our handy screenshot function
|
|
66
|
-
const captureScreenBase64 = async (scale = 1) => {
|
|
67
|
-
let step2 = await captureAndResize(scale);
|
|
89
|
+
const captureScreenBase64 = async (scale = 1, silent = false) => {
|
|
90
|
+
let step2 = await captureAndResize(scale, silent);
|
|
68
91
|
return fs.readFileSync(step2, "base64");
|
|
69
92
|
};
|
|
70
93
|
|
|
71
|
-
const captureScreenPNG = async (scale = 1) => {
|
|
72
|
-
return await captureAndResize(scale);
|
|
94
|
+
const captureScreenPNG = async (scale = 1, silent = false) => {
|
|
95
|
+
return await captureAndResize(scale, silent);
|
|
73
96
|
};
|
|
74
97
|
|
|
75
98
|
const platform = () => {
|
|
@@ -96,6 +119,7 @@ const getMousePosition = async () => {
|
|
|
96
119
|
};
|
|
97
120
|
|
|
98
121
|
module.exports = {
|
|
122
|
+
getPrimaryDisplay,
|
|
99
123
|
captureScreenBase64,
|
|
100
124
|
captureScreenPNG,
|
|
101
125
|
getMousePosition,
|
package/main.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const config = require("./lib/config");
|
|
3
|
+
const { emitter, events } = require("./lib/events.js");
|
|
4
|
+
|
|
5
|
+
if (!config.TD_OVERLAY) {
|
|
6
|
+
require("./index.js");
|
|
7
|
+
} else {
|
|
8
|
+
// Intercept all stdout and stderr calls (works with console as well)
|
|
9
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
10
|
+
process.stdout.write = (...args) => {
|
|
11
|
+
const [data, encoding] = args;
|
|
12
|
+
emitter.emit(
|
|
13
|
+
events.terminal.stdout,
|
|
14
|
+
data.toString(typeof encoding === "string" ? encoding : undefined),
|
|
15
|
+
);
|
|
16
|
+
originalStdoutWrite(...args);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
20
|
+
process.stderr.write = (...args) => {
|
|
21
|
+
const [data, encoding] = args;
|
|
22
|
+
emitter.emit(
|
|
23
|
+
events.terminal.stderr,
|
|
24
|
+
data.toString(typeof encoding === "string" ? encoding : undefined),
|
|
25
|
+
);
|
|
26
|
+
originalStderrWrite(...args);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
require("./lib/overlay.js")
|
|
30
|
+
.electronProcessPromise.then(() => {
|
|
31
|
+
require("./index.js");
|
|
32
|
+
})
|
|
33
|
+
.catch((err) => {
|
|
34
|
+
console.error(err);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "main.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"testdriverai": "./
|
|
7
|
+
"testdriverai": "./main.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node
|
|
11
|
-
"dev": "DEV=true node
|
|
12
|
-
"debug": "DEV=true VERBOSE=true node
|
|
10
|
+
"start": "node main.js",
|
|
11
|
+
"dev": "DEV=true node main.js",
|
|
12
|
+
"debug": "DEV=true VERBOSE=true node main.js",
|
|
13
13
|
"bundle": "node build.mjs"
|
|
14
14
|
},
|
|
15
15
|
"author": "",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"datadog-winston": "^1.6.0",
|
|
23
23
|
"decompress": "^4.2.1",
|
|
24
24
|
"dotenv": "^16.4.5",
|
|
25
|
+
"electron": "^33.0.2",
|
|
25
26
|
"jimp": "^0.22.12",
|
|
26
27
|
"js-yaml": "^4.1.0",
|
|
27
28
|
"mac-screen-capture-permissions": "^2.1.0",
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"marked": "^12.0.1",
|
|
30
31
|
"marked-terminal": "^7.0.0",
|
|
31
32
|
"marky": "^1.2.5",
|
|
33
|
+
"node-ipc": "^12.0.0",
|
|
32
34
|
"node-notifier": "^10.0.1",
|
|
33
35
|
"odiff-bin": "^3.1.2",
|
|
34
36
|
"prompts": "^2.4.2",
|