@letsrunit/cli 0.0.1 → 0.2.4
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 +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +145 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -2
- package/src/index.ts +136 -0
- package/src/run-explore.ts +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# CLI
|
|
2
|
+
|
|
3
|
+
The `letsrunit` CLI allows you to explore websites, generate Gherkin features using AI, and run tests locally.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
### `explore`
|
|
8
|
+
Navigates a target URL and allows interactive discovery of features.
|
|
9
|
+
```bash
|
|
10
|
+
yarn cli explore <url>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### `generate`
|
|
14
|
+
Generates a `.feature` file from natural language instructions.
|
|
15
|
+
```bash
|
|
16
|
+
echo "Login with email and password" | yarn cli generate <url> -o ./features
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### `run`
|
|
20
|
+
Executes a Gherkin feature file against a target URL.
|
|
21
|
+
```bash
|
|
22
|
+
yarn cli run <url> ./features/login.feature
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Options
|
|
26
|
+
- `-v, --verbose`: Enable verbose logging.
|
|
27
|
+
- `-s, --silent`: Only output errors.
|
|
28
|
+
- `-o, --save <path>`: Path to save generated artifacts/features.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { explore, refineSuggestion, generate, run } from '@letsrunit/executor';
|
|
3
|
+
import { makeFeature } from '@letsrunit/gherkin';
|
|
4
|
+
import { Journal, CliSink } from '@letsrunit/journal';
|
|
5
|
+
import { getMailbox } from '@letsrunit/mailbox';
|
|
6
|
+
import { asFilename, randomUUID } from '@letsrunit/utils';
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import * as fs2 from 'fs/promises';
|
|
10
|
+
import fs2__default from 'fs/promises';
|
|
11
|
+
import { dirname, join } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
function disableEcho() {
|
|
15
|
+
process.stdout.write("\x1B[?25l");
|
|
16
|
+
process.stdout.write("\x1B[8m");
|
|
17
|
+
}
|
|
18
|
+
function enableEcho() {
|
|
19
|
+
process.stdout.write("\x1B[0m");
|
|
20
|
+
process.stdout.write("\x1B[?25h");
|
|
21
|
+
}
|
|
22
|
+
function readKey() {
|
|
23
|
+
const { stdin } = process;
|
|
24
|
+
stdin.setRawMode(true);
|
|
25
|
+
stdin.resume();
|
|
26
|
+
stdin.setEncoding("utf8");
|
|
27
|
+
disableEcho();
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const handler = (pressed) => {
|
|
30
|
+
stdin.removeListener("data", handler);
|
|
31
|
+
stdin.setRawMode(false);
|
|
32
|
+
stdin.pause();
|
|
33
|
+
enableEcho();
|
|
34
|
+
resolve(pressed);
|
|
35
|
+
};
|
|
36
|
+
stdin.on("data", handler);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async function readOption(limit) {
|
|
40
|
+
while (true) {
|
|
41
|
+
const key = await readKey();
|
|
42
|
+
if (key === "") return -1;
|
|
43
|
+
const opt = key >= "0" && key <= "9" ? Number(key) : null;
|
|
44
|
+
if (!opt || opt > limit) {
|
|
45
|
+
process.stdout.write("\x1B[33mInvalid option selected\x1B[0m\n");
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
return opt - 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function runExplore(info, actions, storagePath) {
|
|
52
|
+
const { stdout } = process;
|
|
53
|
+
while (actions.length > 0) {
|
|
54
|
+
stdout.write(`
|
|
55
|
+
\x1B[1m${info.title}\x1B[0m
|
|
56
|
+
`);
|
|
57
|
+
stdout.write("What do you want to test? Choose one of the following options:\n");
|
|
58
|
+
let count = 1;
|
|
59
|
+
for (const action of actions) {
|
|
60
|
+
stdout.write(`${count++}. ${action.name}
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
const opt = await readOption(actions.length);
|
|
64
|
+
if (opt < 0) return;
|
|
65
|
+
stdout.write("\n");
|
|
66
|
+
const { status, feature } = await actions[opt].run();
|
|
67
|
+
actions.splice(opt, 1);
|
|
68
|
+
if (storagePath && status === "passed" && feature) {
|
|
69
|
+
await fs2.writeFile(`${storagePath}/${asFilename(feature.name)}.feature`, makeFeature(feature));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/index.ts
|
|
75
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
76
|
+
var { version } = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
77
|
+
var program = new Command();
|
|
78
|
+
function createJournal({ verbose, silent, artifactPath }) {
|
|
79
|
+
const verbosity = verbose ? 3 : silent ? 0 : 1;
|
|
80
|
+
return new Journal(new CliSink({ verbosity, artifactPath }));
|
|
81
|
+
}
|
|
82
|
+
async function readStdin() {
|
|
83
|
+
return await new Promise((resolve) => {
|
|
84
|
+
let data = "";
|
|
85
|
+
process.stdin.setEncoding("utf8");
|
|
86
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
87
|
+
if (isTTY) {
|
|
88
|
+
console.error("Enter instructions. Finish with Ctrl-D (Unix/macOS/Linux) or Ctrl-Z then Enter (Windows).");
|
|
89
|
+
}
|
|
90
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
91
|
+
process.stdin.on("end", () => resolve(data));
|
|
92
|
+
process.stdin.resume();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
program.name("letsrunit").description("Vibe testing done right").version(version);
|
|
96
|
+
program.command("explore").argument("<target>", "Target URL or project").option("-v, --verbose", "Enable verbose logging", false).option("-s, --silent", "Only output errors", false).option("-o, --save <path>", "Path to save .feature file", "").action(async (target, opts) => {
|
|
97
|
+
const journal = createJournal({ ...opts, artifactPath: opts.save });
|
|
98
|
+
const { status } = await explore(target, { headless: false, journal }, async (info, actions) => {
|
|
99
|
+
journal.sink.endSection();
|
|
100
|
+
await runExplore(info, actions, opts.save);
|
|
101
|
+
});
|
|
102
|
+
process.exit(status === "passed" ? 0 : 1);
|
|
103
|
+
});
|
|
104
|
+
program.command("generate").argument("<target>", "Target URL or project").option("-v, --verbose", "Enable verbose logging", false).option("-s, --silent", "Only output errors", false).option("-o, --save <path>", "Path to save .feature file", "").action(async (target, opts) => {
|
|
105
|
+
const instructions = (await readStdin()).trim();
|
|
106
|
+
if (!instructions) {
|
|
107
|
+
console.error("No instructions provided");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const journal = createJournal({ ...opts, artifactPath: opts.save });
|
|
111
|
+
await journal.info("Refining test instructions");
|
|
112
|
+
const suggestion = await refineSuggestion(instructions);
|
|
113
|
+
const { feature, status } = await generate(target, suggestion, { headless: false, journal });
|
|
114
|
+
if (opts.save && feature) {
|
|
115
|
+
await fs2__default.writeFile(`${opts.save}/${asFilename(feature.name)}.feature`, makeFeature(feature));
|
|
116
|
+
}
|
|
117
|
+
process.exit(status === "passed" ? 0 : 1);
|
|
118
|
+
});
|
|
119
|
+
program.command("register").argument("<target>", "Target URL or project").option("-v, --verbose", "Enable verbose logging", false).option("-s, --silent", "Only output errors", false).option("-o, --save <path>", "Path to save .feature file", "").action(async (target, opts) => {
|
|
120
|
+
const journal = createJournal({ ...opts, artifactPath: opts.save });
|
|
121
|
+
const suggestion = {
|
|
122
|
+
name: "Register a new user by email",
|
|
123
|
+
description: [
|
|
124
|
+
"Locate the registration form and fill it out to create a new account.",
|
|
125
|
+
"Confirm the registration email and log in as the user"
|
|
126
|
+
].join("\n"),
|
|
127
|
+
comments: [
|
|
128
|
+
"If no registration button is visible, try locating it through the login form.",
|
|
129
|
+
"The feature is complete when a confirmation email is received and verified and the user is logged in."
|
|
130
|
+
].join("\n")
|
|
131
|
+
};
|
|
132
|
+
const email = getMailbox(randomUUID());
|
|
133
|
+
const { feature, status } = await generate(target, suggestion, { headless: false, journal, accounts: { email } });
|
|
134
|
+
if (opts.save && feature) {
|
|
135
|
+
await fs2__default.writeFile(`${opts.save}/${asFilename(feature.name)}.feature`, makeFeature(feature));
|
|
136
|
+
}
|
|
137
|
+
process.exit(status === "passed" ? 0 : 1);
|
|
138
|
+
});
|
|
139
|
+
program.command("run").argument("<target>", "Target URL or project").argument("<feature>", "Gherkin feature file").option("-v, --verbose", "Enable verbose logging", false).option("-s, --silent", "Only output errors", false).action(async (target, featureFile, opts) => {
|
|
140
|
+
const feature = await fs2__default.readFile(featureFile, "utf-8");
|
|
141
|
+
await run(target, feature, { headless: false, journal: createJournal(opts) });
|
|
142
|
+
});
|
|
143
|
+
program.parse();
|
|
144
|
+
//# sourceMappingURL=index.js.map
|
|
145
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/run-explore.ts","../src/index.ts"],"names":["fs","asFilename","makeFeature"],"mappings":"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","file":"index.js","sourcesContent":["import type { Action, AppInfo } from '@letsrunit/executor';\nimport { makeFeature } from '@letsrunit/gherkin';\nimport { asFilename } from '@letsrunit/utils';\nimport * as fs from 'node:fs/promises';\n\nfunction disableEcho() {\n process.stdout.write('\\x1B[?25l'); // hide cursor\n process.stdout.write('\\x1B[8m'); // hide input\n}\n\nfunction enableEcho() {\n process.stdout.write('\\x1B[0m');\n process.stdout.write('\\x1B[?25h');\n}\n\nfunction readKey(): Promise<string> {\n const { stdin } = process;\n\n stdin.setRawMode(true);\n stdin.resume();\n stdin.setEncoding('utf8');\n disableEcho();\n\n return new Promise<string>((resolve) => {\n const handler = (pressed: string) => {\n stdin.removeListener('data', handler);\n stdin.setRawMode(false);\n stdin.pause();\n enableEcho();\n\n resolve(pressed);\n };\n\n stdin.on('data', handler);\n });\n}\n\nasync function readOption(limit: number): Promise<number> {\n while (true) {\n const key = await readKey();\n if (key === '\\u0003') return -1;\n\n const opt = key >= '0' && key <= '9' ? Number(key) : null;\n\n if (!opt || opt > limit) {\n process.stdout.write('\\x1b[33mInvalid option selected\\x1b[0m\\n');\n continue;\n }\n\n return opt - 1;\n }\n}\n\nexport async function runExplore(info: AppInfo, actions: Action[], storagePath?: string) {\n const { stdout } = process;\n\n while (actions.length > 0) {\n stdout.write(`\\n\\x1b[1m${info.title}\\x1b[0m\\n`);\n stdout.write('What do you want to test? Choose one of the following options:\\n');\n\n let count = 1;\n for (const action of actions) {\n stdout.write(`${count++}. ${action.name}\\n`);\n }\n\n const opt = await readOption(actions.length);\n if (opt < 0) return;\n\n stdout.write('\\n');\n const { status, feature } = await actions[opt].run();\n\n actions.splice(opt, 1);\n\n if (storagePath && status === 'passed' && feature) {\n await fs.writeFile(`${storagePath}/${asFilename(feature.name!)}.feature`, makeFeature(feature));\n }\n }\n}\n","import { explore, generate, refineSuggestion, run } from '@letsrunit/executor';\nimport { makeFeature } from '@letsrunit/gherkin';\nimport { CliSink, Journal } from '@letsrunit/journal';\nimport { getMailbox } from '@letsrunit/mailbox';\nimport { asFilename, randomUUID } from '@letsrunit/utils';\nimport { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport fs from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { runExplore } from './run-explore';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst { version } = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')) as { version: string };\n\nconst program = new Command();\n\ninterface JournalOptions {\n verbose: boolean;\n silent: boolean;\n artifactPath?: string;\n}\n\nfunction createJournal({ verbose, silent, artifactPath }: JournalOptions) {\n const verbosity = verbose ? 3 : silent ? 0 : 1;\n return new Journal(new CliSink({ verbosity, artifactPath }));\n}\n\nasync function readStdin(): Promise<string> {\n return await new Promise((resolve) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n\n const isTTY = Boolean(process.stdin.isTTY);\n if (isTTY) {\n // Interactive input: allow user to type multiple lines and finish with EOF\n console.error('Enter instructions. Finish with Ctrl-D (Unix/macOS/Linux) or Ctrl-Z then Enter (Windows).');\n }\n\n process.stdin.on('data', (chunk) => (data += chunk));\n process.stdin.on('end', () => resolve(data));\n process.stdin.resume();\n });\n}\n\nprogram.name('letsrunit').description('Vibe testing done right').version(version);\n\nprogram\n .command('explore')\n .argument('<target>', 'Target URL or project')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .option('-s, --silent', 'Only output errors', false)\n .option('-o, --save <path>', 'Path to save .feature file', '')\n .action(async (target: string, opts: { verbose: boolean; silent: boolean; save: string }) => {\n const journal = createJournal({ ...opts, artifactPath: opts.save });\n\n const { status } = await explore(target, { headless: false, journal }, async (info, actions) => {\n journal.sink.endSection();\n await runExplore(info, actions, opts.save);\n });\n\n process.exit(status === 'passed' ? 0 : 1);\n });\n\nprogram\n .command('generate')\n .argument('<target>', 'Target URL or project')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .option('-s, --silent', 'Only output errors', false)\n .option('-o, --save <path>', 'Path to save .feature file', '')\n .action(async (target: string, opts: { verbose: boolean; silent: boolean; save: string }) => {\n const instructions = (await readStdin()).trim();\n\n if (!instructions) {\n console.error('No instructions provided');\n process.exit(1);\n }\n\n const journal = createJournal({ ...opts, artifactPath: opts.save });\n\n await journal.info('Refining test instructions');\n const suggestion = await refineSuggestion(instructions);\n\n const { feature, status } = await generate(target, suggestion, { headless: false, journal });\n\n if (opts.save && feature) {\n await fs.writeFile(`${opts.save}/${asFilename(feature.name!)}.feature`, makeFeature(feature));\n }\n\n process.exit(status === 'passed' ? 0 : 1);\n });\n\nprogram\n .command('register')\n .argument('<target>', 'Target URL or project')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .option('-s, --silent', 'Only output errors', false)\n .option('-o, --save <path>', 'Path to save .feature file', '')\n .action(async (target: string, opts: { verbose: boolean; silent: boolean; save: string }) => {\n const journal = createJournal({ ...opts, artifactPath: opts.save });\n\n const suggestion = {\n name: 'Register a new user by email',\n description: [\n 'Locate the registration form and fill it out to create a new account.',\n 'Confirm the registration email and log in as the user',\n ].join('\\n'),\n comments: [\n 'If no registration button is visible, try locating it through the login form.',\n 'The feature is complete when a confirmation email is received and verified and the user is logged in.',\n ].join('\\n'),\n };\n\n const email = getMailbox(randomUUID());\n\n const { feature, status } = await generate(target, suggestion, { headless: false, journal, accounts: { email } });\n\n if (opts.save && feature) {\n await fs.writeFile(`${opts.save}/${asFilename(feature.name!)}.feature`, makeFeature(feature));\n }\n\n process.exit(status === 'passed' ? 0 : 1);\n });\n\nprogram\n .command('run')\n .argument('<target>', 'Target URL or project')\n .argument('<feature>', 'Gherkin feature file')\n .option('-v, --verbose', 'Enable verbose logging', false)\n .option('-s, --silent', 'Only output errors', false)\n .action(async (target: string, featureFile: string, opts: { verbose: boolean; silent: boolean }) => {\n const feature = await fs.readFile(featureFile, 'utf-8');\n await run(target, feature, { headless: false, journal: createJournal(opts) });\n });\n\nprogram.parse();\n"]}
|
package/package.json
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letsrunit/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "Command line interface for letsrunit",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"testing",
|
|
7
|
+
"cli",
|
|
8
|
+
"automation",
|
|
9
|
+
"letsrunit"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/letsrunit-hq/letsrunit.git",
|
|
15
|
+
"directory": "packages/cli"
|
|
16
|
+
},
|
|
17
|
+
"bugs": "https://github.com/letsrunit-hq/letsrunit/issues",
|
|
18
|
+
"homepage": "https://github.com/letsrunit-hq/letsrunit#readme",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"letsrunit": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"bin": {
|
|
28
|
+
"letsrunit": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "../../node_modules/.bin/tsup",
|
|
41
|
+
"dev": "tsx src/index.ts",
|
|
42
|
+
"test": "echo \"No tests yet\""
|
|
43
|
+
},
|
|
44
|
+
"packageManager": "yarn@4.10.3",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@letsrunit/executor": "0.2.4",
|
|
47
|
+
"@letsrunit/gherkin": "0.2.4",
|
|
48
|
+
"@letsrunit/journal": "0.2.4",
|
|
49
|
+
"@letsrunit/mailbox": "0.2.4",
|
|
50
|
+
"@letsrunit/utils": "0.2.4",
|
|
51
|
+
"commander": "^14.0.2"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"tsx": "^4.21.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { explore, generate, refineSuggestion, run } from '@letsrunit/executor';
|
|
2
|
+
import { makeFeature } from '@letsrunit/gherkin';
|
|
3
|
+
import { CliSink, Journal } from '@letsrunit/journal';
|
|
4
|
+
import { getMailbox } from '@letsrunit/mailbox';
|
|
5
|
+
import { asFilename, randomUUID } from '@letsrunit/utils';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { runExplore } from './run-explore';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const { version } = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')) as { version: string };
|
|
15
|
+
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
interface JournalOptions {
|
|
19
|
+
verbose: boolean;
|
|
20
|
+
silent: boolean;
|
|
21
|
+
artifactPath?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createJournal({ verbose, silent, artifactPath }: JournalOptions) {
|
|
25
|
+
const verbosity = verbose ? 3 : silent ? 0 : 1;
|
|
26
|
+
return new Journal(new CliSink({ verbosity, artifactPath }));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readStdin(): Promise<string> {
|
|
30
|
+
return await new Promise((resolve) => {
|
|
31
|
+
let data = '';
|
|
32
|
+
process.stdin.setEncoding('utf8');
|
|
33
|
+
|
|
34
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
35
|
+
if (isTTY) {
|
|
36
|
+
// Interactive input: allow user to type multiple lines and finish with EOF
|
|
37
|
+
console.error('Enter instructions. Finish with Ctrl-D (Unix/macOS/Linux) or Ctrl-Z then Enter (Windows).');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.stdin.on('data', (chunk) => (data += chunk));
|
|
41
|
+
process.stdin.on('end', () => resolve(data));
|
|
42
|
+
process.stdin.resume();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
program.name('letsrunit').description('Vibe testing done right').version(version);
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command('explore')
|
|
50
|
+
.argument('<target>', 'Target URL or project')
|
|
51
|
+
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
52
|
+
.option('-s, --silent', 'Only output errors', false)
|
|
53
|
+
.option('-o, --save <path>', 'Path to save .feature file', '')
|
|
54
|
+
.action(async (target: string, opts: { verbose: boolean; silent: boolean; save: string }) => {
|
|
55
|
+
const journal = createJournal({ ...opts, artifactPath: opts.save });
|
|
56
|
+
|
|
57
|
+
const { status } = await explore(target, { headless: false, journal }, async (info, actions) => {
|
|
58
|
+
journal.sink.endSection();
|
|
59
|
+
await runExplore(info, actions, opts.save);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
process.exit(status === 'passed' ? 0 : 1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command('generate')
|
|
67
|
+
.argument('<target>', 'Target URL or project')
|
|
68
|
+
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
69
|
+
.option('-s, --silent', 'Only output errors', false)
|
|
70
|
+
.option('-o, --save <path>', 'Path to save .feature file', '')
|
|
71
|
+
.action(async (target: string, opts: { verbose: boolean; silent: boolean; save: string }) => {
|
|
72
|
+
const instructions = (await readStdin()).trim();
|
|
73
|
+
|
|
74
|
+
if (!instructions) {
|
|
75
|
+
console.error('No instructions provided');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const journal = createJournal({ ...opts, artifactPath: opts.save });
|
|
80
|
+
|
|
81
|
+
await journal.info('Refining test instructions');
|
|
82
|
+
const suggestion = await refineSuggestion(instructions);
|
|
83
|
+
|
|
84
|
+
const { feature, status } = await generate(target, suggestion, { headless: false, journal });
|
|
85
|
+
|
|
86
|
+
if (opts.save && feature) {
|
|
87
|
+
await fs.writeFile(`${opts.save}/${asFilename(feature.name!)}.feature`, makeFeature(feature));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
process.exit(status === 'passed' ? 0 : 1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command('register')
|
|
95
|
+
.argument('<target>', 'Target URL or project')
|
|
96
|
+
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
97
|
+
.option('-s, --silent', 'Only output errors', false)
|
|
98
|
+
.option('-o, --save <path>', 'Path to save .feature file', '')
|
|
99
|
+
.action(async (target: string, opts: { verbose: boolean; silent: boolean; save: string }) => {
|
|
100
|
+
const journal = createJournal({ ...opts, artifactPath: opts.save });
|
|
101
|
+
|
|
102
|
+
const suggestion = {
|
|
103
|
+
name: 'Register a new user by email',
|
|
104
|
+
description: [
|
|
105
|
+
'Locate the registration form and fill it out to create a new account.',
|
|
106
|
+
'Confirm the registration email and log in as the user',
|
|
107
|
+
].join('\n'),
|
|
108
|
+
comments: [
|
|
109
|
+
'If no registration button is visible, try locating it through the login form.',
|
|
110
|
+
'The feature is complete when a confirmation email is received and verified and the user is logged in.',
|
|
111
|
+
].join('\n'),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const email = getMailbox(randomUUID());
|
|
115
|
+
|
|
116
|
+
const { feature, status } = await generate(target, suggestion, { headless: false, journal, accounts: { email } });
|
|
117
|
+
|
|
118
|
+
if (opts.save && feature) {
|
|
119
|
+
await fs.writeFile(`${opts.save}/${asFilename(feature.name!)}.feature`, makeFeature(feature));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.exit(status === 'passed' ? 0 : 1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
program
|
|
126
|
+
.command('run')
|
|
127
|
+
.argument('<target>', 'Target URL or project')
|
|
128
|
+
.argument('<feature>', 'Gherkin feature file')
|
|
129
|
+
.option('-v, --verbose', 'Enable verbose logging', false)
|
|
130
|
+
.option('-s, --silent', 'Only output errors', false)
|
|
131
|
+
.action(async (target: string, featureFile: string, opts: { verbose: boolean; silent: boolean }) => {
|
|
132
|
+
const feature = await fs.readFile(featureFile, 'utf-8');
|
|
133
|
+
await run(target, feature, { headless: false, journal: createJournal(opts) });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
program.parse();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Action, AppInfo } from '@letsrunit/executor';
|
|
2
|
+
import { makeFeature } from '@letsrunit/gherkin';
|
|
3
|
+
import { asFilename } from '@letsrunit/utils';
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
|
|
6
|
+
function disableEcho() {
|
|
7
|
+
process.stdout.write('\x1B[?25l'); // hide cursor
|
|
8
|
+
process.stdout.write('\x1B[8m'); // hide input
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function enableEcho() {
|
|
12
|
+
process.stdout.write('\x1B[0m');
|
|
13
|
+
process.stdout.write('\x1B[?25h');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readKey(): Promise<string> {
|
|
17
|
+
const { stdin } = process;
|
|
18
|
+
|
|
19
|
+
stdin.setRawMode(true);
|
|
20
|
+
stdin.resume();
|
|
21
|
+
stdin.setEncoding('utf8');
|
|
22
|
+
disableEcho();
|
|
23
|
+
|
|
24
|
+
return new Promise<string>((resolve) => {
|
|
25
|
+
const handler = (pressed: string) => {
|
|
26
|
+
stdin.removeListener('data', handler);
|
|
27
|
+
stdin.setRawMode(false);
|
|
28
|
+
stdin.pause();
|
|
29
|
+
enableEcho();
|
|
30
|
+
|
|
31
|
+
resolve(pressed);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
stdin.on('data', handler);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readOption(limit: number): Promise<number> {
|
|
39
|
+
while (true) {
|
|
40
|
+
const key = await readKey();
|
|
41
|
+
if (key === '\u0003') return -1;
|
|
42
|
+
|
|
43
|
+
const opt = key >= '0' && key <= '9' ? Number(key) : null;
|
|
44
|
+
|
|
45
|
+
if (!opt || opt > limit) {
|
|
46
|
+
process.stdout.write('\x1b[33mInvalid option selected\x1b[0m\n');
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return opt - 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runExplore(info: AppInfo, actions: Action[], storagePath?: string) {
|
|
55
|
+
const { stdout } = process;
|
|
56
|
+
|
|
57
|
+
while (actions.length > 0) {
|
|
58
|
+
stdout.write(`\n\x1b[1m${info.title}\x1b[0m\n`);
|
|
59
|
+
stdout.write('What do you want to test? Choose one of the following options:\n');
|
|
60
|
+
|
|
61
|
+
let count = 1;
|
|
62
|
+
for (const action of actions) {
|
|
63
|
+
stdout.write(`${count++}. ${action.name}\n`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const opt = await readOption(actions.length);
|
|
67
|
+
if (opt < 0) return;
|
|
68
|
+
|
|
69
|
+
stdout.write('\n');
|
|
70
|
+
const { status, feature } = await actions[opt].run();
|
|
71
|
+
|
|
72
|
+
actions.splice(opt, 1);
|
|
73
|
+
|
|
74
|
+
if (storagePath && status === 'passed' && feature) {
|
|
75
|
+
await fs.writeFile(`${storagePath}/${asFilename(feature.name!)}.feature`, makeFeature(feature));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|