testdriverai 4.0.70 → 4.0.72

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 CHANGED
@@ -1,4 +1,4 @@
1
- ![TestDriver.ai](https://github.com/dashcamio/testdriver/assets/318295/2a0ad981-8504-46f0-ad97-60cb6c26f1e7)
1
+ <a href="https://testdriver.ai"><img src="https://github.com/dashcamio/testdriver/assets/318295/2a0ad981-8504-46f0-ad97-60cb6c26f1e7"/></a>
2
2
 
3
3
  # TestDriver.ai
4
4
 
@@ -8,6 +8,10 @@ Next generation autonomous AI agent for end-to-end testing of web & desktop
8
8
 
9
9
  ---
10
10
 
11
+ ```sh
12
+ npm install testdriverai -g
13
+ ```
14
+
11
15
  TestDriver isn't like any test framework you've used before. TestDriver is an OS Agent for QA. TestDriver uses AI vision along with mouse and keyboard emulation to control the entire desktop. It's more like a QA employee than a test framework. This kind of black-box testing has some major advantages:
12
16
 
13
17
  - **Easier set up:** No need to add test IDs or craft complex selectors
package/index.js CHANGED
@@ -459,14 +459,18 @@ const humanInput = async (currentTask, validateAndLoop = false) => {
459
459
  log.log("info", "");
460
460
 
461
461
  let image = await system.captureScreenBase64();
462
- let message = await sdk.req("input", {
463
- input: currentTask,
464
- mousePosition: await system.getMousePosition(),
465
- activeWindow: await system.activeWin(),
466
- image,
467
- });
468
-
469
- log.prettyMarkdown(message);
462
+ const mdStream = log.createMarkdownStreamLogger();
463
+ let message = await sdk.req(
464
+ "input",
465
+ {
466
+ input: currentTask,
467
+ mousePosition: await system.getMousePosition(),
468
+ activeWindow: await system.activeWin(),
469
+ image,
470
+ },
471
+ (chunk) => mdStream.log(chunk),
472
+ );
473
+ mdStream.end();
470
474
 
471
475
  await aiExecute(message, validateAndLoop);
472
476
 
@@ -486,15 +490,19 @@ const generate = async (type, count) => {
486
490
  log.log("info", "");
487
491
 
488
492
  let image = await system.captureScreenBase64();
489
- let message = await sdk.req("generate", {
490
- type,
491
- image,
492
- mousePosition: await system.getMousePosition(),
493
- activeWindow: await system.activeWin(),
494
- count,
495
- });
496
-
497
- log.prettyMarkdown(message);
493
+ const mdStream = log.createMarkdownStreamLogger();
494
+ let message = await sdk.req(
495
+ "generate",
496
+ {
497
+ type,
498
+ image,
499
+ mousePosition: await system.getMousePosition(),
500
+ activeWindow: await system.activeWin(),
501
+ count,
502
+ },
503
+ (chunk) => mdStream.log(chunk),
504
+ );
505
+ mdStream.end();
498
506
 
499
507
  let testPrompts = await parser.findGenerativePrompts(message);
500
508
 
@@ -743,11 +751,17 @@ let summarize = async (error = null) => {
743
751
 
744
752
  log.log("info", chalk.dim("summarizing..."), true);
745
753
 
746
- let reply = await sdk.req("summarize", {
747
- image,
748
- error: error?.toString(),
749
- tasks,
750
- });
754
+ const mdStream = log.createMarkdownStreamLogger();
755
+ let reply = await sdk.req(
756
+ "summarize",
757
+ {
758
+ image,
759
+ error: error?.toString(),
760
+ tasks,
761
+ },
762
+ (chunk) => mdStream.log(chunk),
763
+ );
764
+ mdStream.end();
751
765
 
752
766
  let resultFile = "/tmp/oiResult.log.log";
753
767
  if (process.platform === "win32") {
@@ -755,8 +769,6 @@ let summarize = async (error = null) => {
755
769
  }
756
770
  // write reply to /tmp/oiResult.log.log
757
771
  fs.writeFileSync(resultFile, reply);
758
-
759
- log.prettyMarkdown(reply);
760
772
  };
761
773
 
762
774
  // this function is responsible for saving the regression test script to a file
@@ -801,7 +813,6 @@ ${regression}
801
813
  // it parses the markdown file and executes the codeblocks exactly as if they were
802
814
  // generated by the AI in a single prompt
803
815
  let run = async (file, overwrite = false, shouldExit = true) => {
804
-
805
816
  // parse potential string value for exit
806
817
  shouldExit = shouldExit === "false" ? false : true;
807
818
  overwrite = overwrite === "false" ? false : true;
@@ -883,7 +894,6 @@ ${yaml.dump(step)}
883
894
  await summarize();
884
895
  await exit(false);
885
896
  }
886
-
887
897
  };
888
898
 
889
899
  const promptUser = () => {
package/lib/commands.js CHANGED
@@ -237,23 +237,27 @@ let commands = {
237
237
  log("info", "");
238
238
  log("info", chalk.dim("thinking..."), true);
239
239
 
240
- let response = await sdk.req("hover/text", {
241
- needle: text,
242
- method,
243
- image: await captureScreenBase64(),
244
- intent: action,
245
- button,
246
- clickType,
247
- description,
248
- displayMultiple: 1,
249
- });
240
+ const mdStream = logger.createMarkdownStreamLogger();
241
+ let response = await sdk.req(
242
+ "hover/text",
243
+ {
244
+ needle: text,
245
+ method,
246
+ image: await captureScreenBase64(),
247
+ intent: action,
248
+ button,
249
+ clickType,
250
+ description,
251
+ displayMultiple: 1,
252
+ },
253
+ (chunk) => mdStream.log(chunk),
254
+ );
255
+ mdStream.end();
250
256
 
251
- if (!response.data) {
257
+ if (!response) {
252
258
  throw new AiError("No text on screen matches description", true);
253
259
  } else {
254
- log("info", "");
255
- logger.prettyMarkdown(response.data);
256
- return response.data;
260
+ return response;
257
261
  }
258
262
  },
259
263
  // uses our api to find all images on screen
package/lib/init.js CHANGED
@@ -1,18 +1,18 @@
1
1
  const decompress = require("decompress");
2
2
  const prompts = require("prompts");
3
3
  const path = require("path");
4
- const axios = require("axios");
5
4
  const fs = require("fs");
6
5
  const isValidVersion = require("./valid-version");
7
6
  const os = require("os");
8
7
  const chalk = require("chalk");
8
+ const { Readable } = require("stream");
9
9
 
10
10
  async function getLatestRelease(owner, repo) {
11
11
  try {
12
- const response = await axios.get(
12
+ const response = await fetch(
13
13
  `https://api.github.com/repos/${owner}/${repo}/releases`,
14
14
  );
15
- const releases = response.data;
15
+ const releases = await response.json();
16
16
 
17
17
  // Filter releases that are less than or equal to the version in package.json
18
18
  const validReleases = releases.filter((release) => {
@@ -25,11 +25,7 @@ async function getLatestRelease(owner, repo) {
25
25
 
26
26
  // Download the source code
27
27
  const sourceUrl = latestRelease.tarball_url;
28
- const downloadResponse = await axios({
29
- url: sourceUrl,
30
- method: "GET",
31
- responseType: "stream",
32
- });
28
+ const downloadResponse = await fetch(sourceUrl);
33
29
 
34
30
  const tmpDir = os.tmpdir();
35
31
  const path2 = path.join(
@@ -37,9 +33,12 @@ async function getLatestRelease(owner, repo) {
37
33
  `${repo}-${latestRelease.tag_name}.tar.gz`,
38
34
  );
39
35
  const dest = fs.createWriteStream(path2);
40
- downloadResponse.data.pipe(dest);
41
36
 
42
37
  return new Promise((resolve, reject) => {
38
+ Readable.fromWeb(downloadResponse.body).pipe(dest, {
39
+ end: true,
40
+ });
41
+
43
42
  dest.on("finish", () => {
44
43
  resolve(path2);
45
44
  });
package/lib/logger.js CHANGED
@@ -32,6 +32,50 @@ marked.use(
32
32
 
33
33
  const spaceChar = " ";
34
34
 
35
+ const markedParsePartial = (markdown, start = 0, end = 0) => {
36
+ let result = marked
37
+ .parse(markdown)
38
+ .replace(/^/gm, spaceChar)
39
+ .trimEnd()
40
+ .split("\n");
41
+
42
+ if (end <= 0) {
43
+ end = result.length + end;
44
+ }
45
+ return result.slice(start, end).join("\n");
46
+ };
47
+
48
+ const createMarkdownStreamLogger = () => {
49
+ let buffer = "";
50
+ return {
51
+ log: (chunk) => {
52
+ if (typeof chunk !== "string") {
53
+ log("error", "markdownStreamLogger's log method requires a string");
54
+ log("error", chunk);
55
+ return;
56
+ }
57
+
58
+ const previousConsoleOutput = markedParsePartial(buffer, 0, -1);
59
+
60
+ buffer += chunk;
61
+
62
+ const consoleOutput = markedParsePartial(buffer, 0, -1);
63
+
64
+ process.stdout.write(consoleOutput.replace(previousConsoleOutput, ""));
65
+ },
66
+ end() {
67
+ const previousConsoleOutput = markedParsePartial(buffer, 0, -1);
68
+
69
+ const consoleOutput = markedParsePartial(buffer);
70
+
71
+ process.stdout.write(consoleOutput.replace(previousConsoleOutput, ""));
72
+ process.stdout.write("\n\n");
73
+ buffer = "";
74
+ log("silly", consoleOutput);
75
+ },
76
+ };
77
+ };
78
+
35
79
  const prettyMarkdown = (markdown) => {
36
80
  if (typeof markdown !== "string") {
37
81
  log("error", "prettyMarkdown requires a string");
@@ -100,6 +144,7 @@ let loggy = {
100
144
  log,
101
145
  setDepth,
102
146
  prettyMarkdown,
147
+ createMarkdownStreamLogger,
103
148
  };
104
149
 
105
150
  module.exports = loggy;
package/lib/sdk.js CHANGED
@@ -1,32 +1,44 @@
1
1
  const config = require("./config");
2
2
  const chalk = require("chalk");
3
- const axios = require("axios");
4
3
  const session = require("./session");
5
4
  const package = require("../package.json");
6
5
  const version = package.version;
7
6
 
8
7
  const root = config["TD_API_ROOT"];
8
+ const shouldStream = config["TD_STREAM_RESPONSES"];
9
9
 
10
10
  // let token = null;
11
11
 
12
- let outputError = (e) => {
13
- console.log(chalk.red(e.code || e.response.data.code));
12
+ const outputError = async (error) => {
13
+ if (error instanceof Response) {
14
+ console.log(chalk.red(error.status), chalk.red(error.statusText));
15
+ await parseBody(error)
16
+ .then((body) => console.log(chalk.red(body)))
17
+ .catch(() => {});
18
+ } else {
19
+ console.error("Error:", error);
20
+ }
21
+ };
14
22
 
15
- if (e.response) {
16
- console.log(
17
- chalk.red(e.response?.status),
18
- chalk.red(e.response?.statusText),
19
- );
20
- if (e.response.data) {
21
- console.log(e.response.data);
22
- }
23
- if (e.response.data.message) {
24
- console.log(e.response.data.message);
23
+ const parseBody = async (response, body) => {
24
+ const contentType = response.headers.get("Content-Type")?.toLowerCase();
25
+ try {
26
+ if (body === null || body === undefined) {
27
+ if (!contentType.includes("json") && !contentType.includes("text")) {
28
+ return await response.arrayBuffer();
29
+ }
30
+ if (contentType.includes("json")) {
31
+ return await response.json();
32
+ }
33
+ return await response.text();
25
34
  }
26
- if (e.response.data.problems) {
27
- console.log("-----");
28
- console.log(e.response.data.problems.join("\n"));
35
+ if (typeof body === "string" && contentType.includes("json")) {
36
+ return JSON.parse(body);
29
37
  }
38
+ return body;
39
+ } catch (err) {
40
+ console.log(chalk.red("Parsing Error", err));
41
+ throw err;
30
42
  }
31
43
  };
32
44
 
@@ -38,10 +50,9 @@ let auth = async () => {
38
50
  // process.exit(1);
39
51
  // }
40
52
 
41
- let config = {
53
+ const url = [root, "auth/exchange-api-key"].join("/");
54
+ const config = {
42
55
  method: "post",
43
- maxBodyLength: Infinity,
44
- url: [root, "auth/exchange-api-key"].join("/"),
45
56
  headers: {
46
57
  "Content-Type": "application/json",
47
58
  },
@@ -49,15 +60,15 @@ let auth = async () => {
49
60
  };
50
61
 
51
62
  try {
52
- await axios.request(config);
63
+ await fetch(url, config);
53
64
  // token = res.data.token;
54
- } catch (e) {
55
- outputError(e);
65
+ } catch (error) {
66
+ await outputError(error);
56
67
  process.exit(1);
57
68
  }
58
69
  };
59
70
 
60
- let req = async (path, data) => {
71
+ const req = async (path, data, onChunk) => {
61
72
  // for each value of data, if it is empty remove it
62
73
  for (let key in data) {
63
74
  if (!data[key]) {
@@ -65,48 +76,54 @@ let req = async (path, data) => {
65
76
  }
66
77
  }
67
78
 
68
- data.session = session.get()?.id;
69
- data = JSON.stringify(data);
70
-
71
- let dataCopy = JSON.parse(data);
72
- delete dataCopy.image;
79
+ const url = path.startsWith("/api")
80
+ ? [root, path].join("")
81
+ : [root, "api", "v" + version, "testdriver", path].join("/");
73
82
 
74
- let url = [root, "api", "v" + version, "testdriver", path].join("/");
75
-
76
- let config = {
83
+ const config = {
77
84
  method: "post",
78
- maxBodyLength: Infinity,
79
- url,
80
- headers: {
81
- "Content-Type": "application/json",
82
- // 'Authorization': 'Bearer ' + token @todo add-auth
83
- },
84
- data,
85
+ headers: { "Content-Type": "application/json" },
86
+ body: JSON.stringify({
87
+ ...data,
88
+ session: session.get()?.id,
89
+ stream: typeof onChunk === "function",
90
+ }),
85
91
  };
86
92
 
87
- let response;
88
- let redirect = null;
89
93
  try {
90
- response = await axios.request(config);
91
- } catch (e) {
92
- if (e.response?.status === 301) {
93
- config.url = root + e.response.data;
94
- redirect = config;
95
- } else {
96
- outputError(e);
94
+ const response = await fetch(url, config);
95
+ if (response.status === 301) {
96
+ const redirectUrl = await response.text();
97
+ return req(redirectUrl, data, onChunk);
98
+ }
99
+ if (response.status >= 300) {
100
+ throw response;
97
101
  }
98
- }
99
102
 
100
- if (redirect) {
101
- response = await axios.request(redirect).catch((e) => {
102
- outputError(e);
103
- });
104
- }
103
+ let result;
104
+ if (onChunk) {
105
+ result = "";
106
+ const reader = response.body.getReader();
107
+ while (true) {
108
+ const { done, value } = await reader.read();
109
+ if (done) {
110
+ break;
111
+ }
112
+
113
+ const chunk = new TextDecoder().decode(value);
114
+ result += chunk;
115
+ if (shouldStream) {
116
+ await onChunk(chunk);
117
+ }
118
+ }
119
+ if (!shouldStream) {
120
+ await onChunk(result);
121
+ }
122
+ }
105
123
 
106
- if (!response) {
107
- return;
108
- } else {
109
- return response.data;
124
+ return parseBody(response, result);
125
+ } catch (error) {
126
+ await outputError(error);
110
127
  }
111
128
  };
112
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "4.0.70",
3
+ "version": "4.0.72",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -17,7 +17,6 @@
17
17
  "dependencies": {
18
18
  "@electerm/strip-ansi": "^1.0.0",
19
19
  "active-win": "^8.2.1",
20
- "axios": "^1.6.8",
21
20
  "chalk": "^4.1.2",
22
21
  "cli-progress": "^3.12.0",
23
22
  "datadog-winston": "^1.6.0",