testdriverai 1.0.0 → 4.0.0

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/parser.js ADDED
@@ -0,0 +1,100 @@
1
+ // parses markdown content to find code blocks, and then extracts yaml from those code blocks
2
+ const Parser = require('markdown-parser');
3
+ const yaml = require('js-yaml');
4
+
5
+ let parser = new Parser();
6
+
7
+ // use markdown parser to find code blocks within AI response
8
+ const findCodeBlocks = (markdownContent) => {
9
+
10
+ return new Promise((resolve, reject) => {
11
+ parser.parse(markdownContent, (err, result) => {
12
+
13
+ if (err) {
14
+ return reject(err);
15
+ }
16
+
17
+ let codes = result.codes.filter((code) => {
18
+ return code.code.indexOf('yml') > -1 || code.code.indexOf('yaml') > -1;
19
+ })
20
+
21
+ return resolve(codes);
22
+
23
+ });
24
+
25
+ });
26
+
27
+ }
28
+
29
+ // parse the yml from the included codeblock and clean it up
30
+ const getYAMLFromCodeBlock = function(codeblock) {
31
+
32
+ let lines = codeblock.code.split('\n');
33
+
34
+ // if first line is yaml or yml, remove it
35
+ if (lines[0].indexOf('yaml') > -1 || lines[0].indexOf('yml') > -1) {
36
+ lines.shift();
37
+ }
38
+
39
+ // count the whitespace in each line, and remove the line if it's all whitespace
40
+ lines = lines.filter((line) => {
41
+ return line.trim().length > 0 && line.trim()[0] !== ',';
42
+ // sometimes it produces yaml with breaks, or just a single comma
43
+ });
44
+
45
+ return lines.join('\n');
46
+
47
+ }
48
+
49
+ // do the actual parsing
50
+ // this library is very strict
51
+ // note that errors are sent to the AI will it may self-heal
52
+ const parseYAML = async function(inputYaml) {
53
+
54
+ let doc = await yaml.load(inputYaml);
55
+ return doc;
56
+
57
+ };
58
+
59
+ module.exports = {
60
+ findCodeBlocks,
61
+ getYAMLFromCodeBlock,
62
+ getCommands: async function(codeBlock) {
63
+
64
+ const yml = getYAMLFromCodeBlock(codeBlock);
65
+ let yamlArray = await parseYAML(yml);
66
+
67
+ let steps = yamlArray?.steps;
68
+
69
+ if (steps) {
70
+
71
+ let commands = [];
72
+
73
+ // combine them all as if they were a single step
74
+ steps.forEach((s) => {
75
+ commands = commands.concat(s.commands);
76
+ });
77
+
78
+ // filter undefined values
79
+ commands = commands.filter((r) => {
80
+ return r;
81
+ });
82
+
83
+ if (!commands.length) {
84
+ throw new Error('No actions found in yaml. Individual commands must be under the `commands` key.');
85
+ }
86
+
87
+ return commands;
88
+
89
+ } else {
90
+ let commands = yamlArray?.commands;
91
+
92
+ if (!commands?.length) {
93
+ throw new Error('No actions found in yaml. Individual commands must be under the `commands` key.');
94
+ }
95
+
96
+ return commands;
97
+
98
+ }
99
+ }
100
+ }
package/lib/redraw.js ADDED
@@ -0,0 +1,51 @@
1
+ const {captureScreenPNG} = require('./system');
2
+
3
+ const Jimp = require('jimp');
4
+
5
+ async function compareImages(image1Url, image2Url) {
6
+ const image1 = await Jimp.read(image1Url);
7
+ const image2 = await Jimp.read(image2Url);
8
+
9
+ // Perceived distance
10
+ const distance = Jimp.distance(image1, image2);
11
+ // Pixel difference
12
+ const diff = Jimp.diff(image1, image2);
13
+
14
+ if (diff.percent < 0.15) {
15
+ return false;
16
+ } else {
17
+ return true;
18
+ }
19
+ }
20
+
21
+ let startImage = null;
22
+
23
+ async function start() {
24
+ startImage = await captureScreenPNG();
25
+ return startImage;
26
+ }
27
+
28
+ function wait(timeoutMs) {
29
+ return new Promise(async (resolve, reject) => {
30
+ const startTime = Date.now();
31
+
32
+ async function checkCondition() {
33
+
34
+ let nowImage = await captureScreenPNG();
35
+ let result = await compareImages(startImage, nowImage);
36
+
37
+ if (result) {
38
+ resolve('Condition met');
39
+ } else if (Date.now() - startTime >= timeoutMs) {
40
+ resolve('Timeout reached');
41
+ } else {
42
+ setTimeout(checkCondition, 0);
43
+ }
44
+
45
+ }
46
+
47
+ checkCondition();
48
+ });
49
+ }
50
+
51
+ module.exports = {start, wait};
package/lib/sdk.js ADDED
@@ -0,0 +1,113 @@
1
+ // custom "sdk" for calling our API
2
+ const root = "http://api.testdriver.ai";
3
+ const chalk = require('chalk');
4
+ const axios = require('axios');
5
+ const session = require('./session')
6
+ const package = require('../package.json');
7
+ const version = package.version;
8
+
9
+ let token = null;
10
+
11
+ let outputError = (e) => {
12
+
13
+ console.log(chalk.red(e.code || e.response.data.code))
14
+
15
+ if (e.response) {
16
+
17
+ console.log(chalk.red(e.response?.status), chalk.red(e.response?.statusText))
18
+ if(e.response.data) { console.log(e.response.data) }
19
+ if(e.response.data.message) { console.log(e.response.data.message) }
20
+ if (e.response.data.problems) {
21
+ console.log('-----')
22
+ console.log(e.response.data.problems.join('\n'))
23
+ }
24
+ }
25
+ }
26
+
27
+ let auth = async () => {
28
+
29
+ // data.apiKey = process.env.DASHCAM_API_KEY; @todo add-auth
30
+
31
+ if (!data.apiKey) {
32
+ console.log(chalk.red('API key not found. Set DASHCAM_API_KEY in your environment.'));
33
+ process.exit(1);
34
+ }
35
+
36
+ let config = {
37
+ method: 'post',
38
+ maxBodyLength: Infinity,
39
+ url: [root, 'auth/exchange-api-key'].join('/'),
40
+ headers: {
41
+ 'Content-Type': 'application/json'
42
+ },
43
+ data
44
+ };
45
+
46
+ // this is a dashcam api url
47
+ try {
48
+ let res = await axios.request(config);
49
+ token = res.data.token;
50
+ } catch (e) {
51
+ outputError(e);
52
+ process.exit(1);
53
+ }
54
+
55
+ }
56
+
57
+ let req = async (path, data) => {
58
+
59
+ // for each value of data, if it is empty remove it
60
+ for (let key in data) {
61
+ if (!data[key]) {
62
+ delete data[key];
63
+ }
64
+ }
65
+
66
+ data.session = session.get()?.id;
67
+ data = JSON.stringify(data);
68
+
69
+ let dataCopy = JSON.parse(data);
70
+ delete dataCopy.image
71
+
72
+ let url = [root, 'api', 'v' + version, 'testdriver', path].join('/');
73
+
74
+ let config = {
75
+ method: 'post',
76
+ maxBodyLength: Infinity,
77
+ url,
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ // 'Authorization': 'Bearer ' + token @todo add-auth
81
+ },
82
+ data
83
+ };
84
+
85
+ let response;
86
+ let redirect = null
87
+ try {
88
+ response = await axios.request(config);
89
+ } catch (e) {
90
+
91
+ if (e.response?.status === 301) {
92
+ config.url = root + e.response.data;
93
+ redirect = config;
94
+ } else {
95
+ outputError(e);
96
+ }
97
+
98
+ }
99
+
100
+ if (redirect) {
101
+ response = await axios.request(redirect).catch(e => {
102
+ outputError(e);
103
+ });
104
+ }
105
+
106
+ if (!response) {
107
+ return;
108
+ } else {
109
+ return response.data;
110
+ }
111
+ }
112
+
113
+ module.exports = {req, auth};
package/lib/session.js ADDED
@@ -0,0 +1,10 @@
1
+ let session = null;
2
+
3
+ module.exports = {
4
+ get: () => {
5
+ return session;
6
+ },
7
+ set: (s) => {
8
+ session = s;
9
+ }
10
+ };
package/lib/speak.js ADDED
@@ -0,0 +1,14 @@
1
+ const say = require('say');
2
+
3
+ module.exports = (message) => {
4
+
5
+ if (process.env["TD_SPEAK"]) {
6
+ say.stop();
7
+ if (process.platform === 'darwin') {
8
+ say.speak(message, 'Fred', 1.2);
9
+ } else {
10
+ say.speak(message);
11
+ }
12
+ }
13
+
14
+ }
package/lib/system.js ADDED
@@ -0,0 +1,112 @@
1
+ // utilities for getting information about the system
2
+ const fs = require('fs')
3
+ const os = require('os')
4
+ const path = require('path')
5
+ const screenshot = require('screenshot-desktop')
6
+ const si = require('systeminformation');
7
+ const activeWindow = require('active-win');
8
+ const sharp = require('sharp')
9
+ const robot = require('robotjs');
10
+
11
+ let displayMultiple = 0;
12
+ let primaryDisplay = null;
13
+
14
+ // get the primary display
15
+ // this is the only display we ever target, because fuck it
16
+ // the vm only has one and most people only have one
17
+ const getPrimaryDisplay = async () => {
18
+
19
+ // calculate scaling resolution
20
+ let graphics = await si.graphics();
21
+ let primaryDisplay = graphics.displays.find((display) => display.main == true);
22
+
23
+ return primaryDisplay;
24
+ }
25
+
26
+ const getSystemInformationOsInfo = async() => {
27
+ return await si.osInfo();
28
+ }
29
+
30
+ // this hepls us understand how to scale things for retina screens
31
+ const calculateDisplayMultiple = async () => {
32
+ let primaryDisplay = await getPrimaryDisplay();
33
+ displayMultiple = primaryDisplay.currentResX / primaryDisplay.resolutionX;
34
+ };
35
+
36
+ const getDisplayMultiple = async () => {
37
+
38
+ if (!displayMultiple) {
39
+ await calculateDisplayMultiple();
40
+ }
41
+
42
+ return displayMultiple;
43
+ };
44
+
45
+ const tmpFilename = () => {
46
+ return path.join(os.tmpdir(), `${new Date().getTime() + Math.random()}.png`);
47
+ }
48
+
49
+ // our handy screenshot function
50
+ const captureScreenBase64 = async () => {
51
+
52
+ let primaryDisplay = await getPrimaryDisplay();
53
+
54
+ let step1 = tmpFilename();
55
+ let step2 = tmpFilename();
56
+
57
+ await screenshot({ filename: step1, format: 'png' });
58
+
59
+ // resize to 1:1 px ratio
60
+ await sharp(step1)
61
+ .resize(primaryDisplay.currentResX, primaryDisplay.currentResY)
62
+ .toFile(step2);
63
+
64
+ let image = fs.readFileSync(step2, "base64");
65
+
66
+ return image;
67
+
68
+ };
69
+
70
+ const captureScreenPNG = async () => {
71
+
72
+ let step1 = tmpFilename();
73
+ await screenshot({ filename: step1, format: 'png' });
74
+
75
+ return step1;
76
+
77
+ }
78
+
79
+ const platform = () => {
80
+ let platform = process.platform;
81
+ if (platform === 'darwin') {
82
+ platform = 'mac';
83
+ } else if (platform === 'win32') {
84
+ platform = 'windows';
85
+ } else if (platform === 'linux') {
86
+ platform = 'linux';
87
+ } else {
88
+ throw new Error('Unsupported platform');
89
+ }
90
+ return platform;
91
+ }
92
+
93
+ // this is the focused window
94
+ const activeWin = async () => {
95
+ return await activeWindow();
96
+ }
97
+
98
+ const getMousePosition = async () => {
99
+ return await robot.getMousePos();
100
+ }
101
+
102
+ module.exports = {
103
+ captureScreenBase64,
104
+ captureScreenPNG,
105
+ getDisplayMultiple,
106
+ calculateDisplayMultiple,
107
+ getMousePosition,
108
+ primaryDisplay,
109
+ activeWin,
110
+ platform,
111
+ getSystemInformationOsInfo
112
+ }
@@ -0,0 +1,19 @@
1
+ const semver = require('semver')
2
+ const package = require('../package.json');
3
+
4
+ // Function to check if the new version's minor version is >= current version's minor version
5
+ module.exports = (inputVersion) => {
6
+
7
+ const currentParsed = semver.parse(package.version);
8
+ const inputParsed = semver.parse(inputVersion.replace('v', ''));
9
+
10
+ if (!currentParsed || !inputParsed) {
11
+ throw new Error('Invalid version format');
12
+ }
13
+
14
+ // Compare major and minor versions
15
+ if (inputParsed.major === currentParsed.major && inputParsed.minor >= currentParsed.minor) {
16
+ return true;
17
+ }
18
+ return false;
19
+ }
package/package.json CHANGED
@@ -1,11 +1,57 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "1.0.0",
3
+ "version": "4.0.0",
4
+ "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
4
5
  "main": "index.js",
6
+ "bin": {
7
+ "testdriver": "./index.js"
8
+ },
5
9
  "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1"
10
+ "start": "node index.js",
11
+ "dev": "DEV=true node index.js",
12
+ "debug": "DEV=true VERBOSE=true node index.js",
13
+ "bundle": "node build.mjs",
14
+ "postinstall": "node postinstall.js"
7
15
  },
8
16
  "author": "",
9
17
  "license": "ISC",
10
- "description": ""
18
+ "dependencies": {
19
+ "@electerm/strip-ansi": "^1.0.0",
20
+ "active-win": "^8.2.1",
21
+ "axios": "^1.6.8",
22
+ "chalk": "^4.1.2",
23
+ "cli-progress": "^3.12.0",
24
+ "datadog-winston": "^1.6.0",
25
+ "decompress": "^4.2.1",
26
+ "dotenv": "^16.4.5",
27
+ "jimp": "^0.22.12",
28
+ "js-yaml": "^4.1.0",
29
+ "markdown-parser": "0.0.8",
30
+ "marked": "^12.0.1",
31
+ "marked-terminal": "^7.0.0",
32
+ "node-notifier": "^10.0.1",
33
+ "prompts": "^2.4.2",
34
+ "remark-parse": "^11.0.0",
35
+ "rimraf": "^5.0.5",
36
+ "robotjs": "^0.6.0",
37
+ "say": "^0.16.0",
38
+ "screenshot-desktop": "^1.15.0",
39
+ "semver": "^7.6.2",
40
+ "sharp": "^0.33.4",
41
+ "systeminformation": "^5.22.7",
42
+ "tmp": "^0.2.3",
43
+ "uuid": "^10.0.0",
44
+ "winston": "^3.13.0"
45
+ },
46
+ "devDependencies": {
47
+ "esbuild": "0.20.2",
48
+ "esbuild-plugin-fileloc": "^0.0.6",
49
+ "node-addon-api": "^8.0.0",
50
+ "node-gyp": "^10.1.0"
51
+ },
52
+ "optionalDependencies": {
53
+ "@esbuild/linux-x64": "^0.21.5",
54
+ "@img/sharp-libvips-win32-x64": "^1.0.2",
55
+ "@img/sharp-win32-x64": "^0.33.4"
56
+ }
11
57
  }
package/postinstall.js ADDED
@@ -0,0 +1,20 @@
1
+ let platform = require('os').platform();
2
+ let exec = require('child_process').exec;
3
+
4
+ if (platform !== 'darwin') {
5
+ console.log('TestDriver Setup: Skipping codesign becasue not on Mac');
6
+ return;
7
+ }
8
+
9
+ console.log('TestDriver Setup: Codesigning terminal-notifier.app');
10
+
11
+ let signScript = `codesign --sign - --force --deep node_modules/node-notifier/vendor/mac.noindex/terminal-notifier.app`;
12
+
13
+ exec(signScript, (error, stdout, stderr) => {
14
+ if (error) {
15
+ console.error(`exec error: ${error}`);
16
+ return;
17
+ }
18
+ console.log(`stdout: ${stdout}`);
19
+ console.error(`stderr: ${stderr}`);
20
+ });
@@ -0,0 +1,70 @@
1
+ const Jimp = require("jimp");
2
+ const path = require("path");
3
+ cv = require('./opencv.js');
4
+
5
+ async function findTemplateImage(haystack, needle, threshold) {
6
+ try {
7
+ const positions = [];
8
+
9
+ const imageSource = await Jimp.read(path.join(haystack));
10
+ const imageTemplate = await Jimp.read(path.join(needle));
11
+
12
+ const templ = cv.matFromImageData(imageTemplate.bitmap);
13
+ let src = cv.matFromImageData(imageSource.bitmap);
14
+ let processedImage = new cv.Mat();
15
+ let mask = new cv.Mat();
16
+
17
+ cv.matchTemplate(src, templ, processedImage, cv.TM_CCOEFF_NORMED, mask);
18
+
19
+ cv.threshold(
20
+ processedImage,
21
+ processedImage,
22
+ threshold,
23
+ 1,
24
+ cv.THRESH_BINARY
25
+ );
26
+ processedImage.convertTo(processedImage, cv.CV_8UC1);
27
+ let contours = new cv.MatVector();
28
+ let hierarchy = new cv.Mat();
29
+
30
+ cv.findContours(
31
+ processedImage,
32
+ contours,
33
+ hierarchy,
34
+ cv.RETR_EXTERNAL,
35
+ cv.CHAIN_APPROX_SIMPLE
36
+ );
37
+
38
+ for (let i = 0; i < contours.size(); ++i) {
39
+ let [x, y] = contours.get(i).data32S; // Contains the points
40
+ positions.push({
41
+ x,
42
+ y,
43
+ height: templ.rows,
44
+ width: templ.cols,
45
+ centerX: x + templ.cols / 2,
46
+ centerY: y + templ.rows / 2,
47
+ });
48
+ }
49
+
50
+ src.delete();
51
+ mask.delete();
52
+ templ.delete();
53
+
54
+ return positions;
55
+ } catch (err) {
56
+ console.error('OpenCV threw an error');
57
+ }
58
+ }
59
+
60
+ function onRuntimeInitialized() {
61
+ }
62
+
63
+ // Finally, load the open.js as before. The function `onRuntimeInitialized` contains our program.
64
+ Module = {
65
+ onRuntimeInitialized,
66
+ };
67
+
68
+ module.exports = {
69
+ findTemplateImage
70
+ }