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/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) < 2000) {
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 imageIsDifferent(image1Url, image2Url) {
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
- if (diffPercentage > 10) {
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
- screenHasRedrawn = await imageIsDifferent(startImage, nowImage);
128
+ diffPercent = await imageDiffPercent(startImage, nowImage);
129
+ screenHasRedrawn = diffPercent > redrawThresholdPercent;
126
130
  }
127
131
 
128
- if (screenHasRedrawn && networkSettled) {
129
- clearInterval(watchNetwork);
130
- resolve("Condition met");
131
- } else if (Date.now() - timeElapsed > timeoutMs) {
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
- if (timeElapsed > 3000) {
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 await response.json();
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
- const chunk = new TextDecoder().decode(value);
138
+ let chunk = new TextDecoder().decode(value);
139
+
114
140
  result += chunk;
115
- if (shouldStream) {
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
- if (!shouldStream) {
120
- await onChunk(result);
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
- let primaryDisplay = await getPrimaryDisplay();
36
-
37
- let step1 = tmpFilename();
38
- let step2 = tmpFilename();
39
-
40
- if (process.env["DEV"]) {
41
- console.log(step2)
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.0.80",
3
+ "version": "4.1.1",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
- "main": "index.js",
5
+ "main": "main.js",
6
6
  "bin": {
7
- "testdriverai": "./index.js"
7
+ "testdriverai": "./main.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node index.js",
11
- "dev": "DEV=true node index.js",
12
- "debug": "DEV=true VERBOSE=true node index.js",
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",