@popoverai/dotrequirements 0.19.0 → 0.20.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/README.md +31 -0
- package/dist/cli.js +6 -0
- package/dist/commands/browsertest.d.ts +6 -0
- package/dist/commands/browsertest.js +167 -0
- package/dist/utils/project-settings.d.ts +11 -0
- package/dist/utils/project-settings.js +25 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,6 +164,37 @@ dotreq test
|
|
|
164
164
|
dotreq test --file .requirements/auth.requirements.md
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
+
### `dotreq browsertest`
|
|
168
|
+
|
|
169
|
+
Run browser-based acceptance tests against requirements. Uses AI-powered browser automation to verify that your application behaves as specified.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
dotreq browsertest LOGIN-1
|
|
173
|
+
dotreq browsertest LOGIN-1 https://example.com
|
|
174
|
+
dotreq browsertest LOGIN-1 --json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Options:**
|
|
178
|
+
|
|
179
|
+
| Option | Description |
|
|
180
|
+
|--------|-------------|
|
|
181
|
+
| `--json` | Output results as JSON (for CI/CD integration) |
|
|
182
|
+
|
|
183
|
+
**Configuration required:**
|
|
184
|
+
|
|
185
|
+
Browser testing requires credentials in `project-settings.json`:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"defaultURL": "https://your-app.com",
|
|
190
|
+
"browserTest": {
|
|
191
|
+
"geminiApiKey": "your-gemini-api-key"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Optional settings: `vercelBypassSecret`, `browserbaseApiKey`, `browserbaseProjectId`.
|
|
197
|
+
|
|
167
198
|
### `dotreq mcp-setup`
|
|
168
199
|
|
|
169
200
|
Configure the MCP server for AI assistants (Claude Code, Cursor, etc.).
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { linkCommand } from './commands/link.js';
|
|
|
5
5
|
import { pullCommand } from './commands/pull.js';
|
|
6
6
|
import { pushCommand } from './commands/push.js';
|
|
7
7
|
import { testCommand } from './commands/test.js';
|
|
8
|
+
import { browserTestCommand } from './commands/browsertest.js';
|
|
8
9
|
import { mcpCommand } from './commands/mcp.js';
|
|
9
10
|
import { mcpSetupCommand } from './commands/mcp-setup.js';
|
|
10
11
|
import { loadEnvFile } from './utils/env.js';
|
|
@@ -70,6 +71,11 @@ program
|
|
|
70
71
|
.description('Validate requirements files in .requirements/')
|
|
71
72
|
.option('-f, --file <path>', 'Specific file to validate')
|
|
72
73
|
.action(wrapCommand(testCommand));
|
|
74
|
+
program
|
|
75
|
+
.command('browsertest <requirement-key> [url]')
|
|
76
|
+
.description('Run browser-based acceptance test for a requirement')
|
|
77
|
+
.option('--json', 'Output results as JSON')
|
|
78
|
+
.action(wrapCommand(browserTestCommand));
|
|
73
79
|
program
|
|
74
80
|
.command('mcp')
|
|
75
81
|
.description('Start the MCP (Model Context Protocol) server for AI assistant integration')
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { getProjectInfo, readProjectSettings, } from '../utils/project-settings.js';
|
|
4
|
+
import { loadAllRequirements, getRequirementTree, formatRequirement, } from '../mcp/requirements.js';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
export async function browserTestCommand(requirementKey, url, options) {
|
|
7
|
+
// 1. Find project and load settings
|
|
8
|
+
const projectInfo = getProjectInfo();
|
|
9
|
+
if (!projectInfo) {
|
|
10
|
+
console.error('Error: No dotrequirements project found.');
|
|
11
|
+
console.log('Run "dotrequirements init" to create one.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
let settings;
|
|
15
|
+
try {
|
|
16
|
+
settings = readProjectSettings(projectInfo.rootPath);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
// Settings file exists but is invalid
|
|
20
|
+
console.error(`Error: ${err.message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
// 2. Determine URL
|
|
24
|
+
const targetURL = url || settings?.defaultURL;
|
|
25
|
+
if (!targetURL) {
|
|
26
|
+
console.error('Error: No URL provided and no defaultURL in project-settings.json.');
|
|
27
|
+
console.log('Either provide a URL argument or add "defaultURL" to .requirements/project-settings.json');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
// 3. Check for required credentials
|
|
31
|
+
const geminiApiKey = settings?.browserTest?.geminiApiKey;
|
|
32
|
+
if (!geminiApiKey) {
|
|
33
|
+
console.error('Error: browserTest.geminiApiKey not found in project-settings.json.');
|
|
34
|
+
console.log('Add "browserTest": { "geminiApiKey": "..." } to .requirements/project-settings.json');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
// 4. Load requirements
|
|
38
|
+
const { flattened } = await loadAllRequirements(projectInfo.rootPath);
|
|
39
|
+
if (flattened.length === 0) {
|
|
40
|
+
console.error('Error: No requirements found in .requirements/');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
// 5. Find the requirement tree
|
|
44
|
+
const requirementTree = getRequirementTree(flattened, requirementKey);
|
|
45
|
+
if (requirementTree.length === 0) {
|
|
46
|
+
console.error(`Error: Requirement "${requirementKey}" not found.`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
// 6. Format each requirement as an assertion
|
|
50
|
+
const assertions = requirementTree.map((req) => formatRequirement(req));
|
|
51
|
+
// 7. Build environment with injected secrets
|
|
52
|
+
const env = { ...process.env };
|
|
53
|
+
env.GEMINI_API_KEY = geminiApiKey;
|
|
54
|
+
if (settings?.browserTest?.vercelBypassSecret) {
|
|
55
|
+
env.VERCEL_AUTOMATION_BYPASS_SECRET = settings.browserTest.vercelBypassSecret;
|
|
56
|
+
}
|
|
57
|
+
if (settings?.browserTest?.browserbaseApiKey) {
|
|
58
|
+
env.BROWSERBASE_API_KEY = settings.browserTest.browserbaseApiKey;
|
|
59
|
+
}
|
|
60
|
+
if (settings?.browserTest?.browserbaseProjectId) {
|
|
61
|
+
env.BROWSERBASE_PROJECT_ID = settings.browserTest.browserbaseProjectId;
|
|
62
|
+
}
|
|
63
|
+
// 8. Run browser-automation
|
|
64
|
+
if (!options.json) {
|
|
65
|
+
console.log(`Testing ${requirementKey} against ${targetURL}...\n`);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await execFileAsync('npx', ['@popoverai/browser-automation', 'test', targetURL, ...assertions], { env, maxBuffer: 10 * 1024 * 1024 });
|
|
69
|
+
// 9. Parse results
|
|
70
|
+
let results;
|
|
71
|
+
try {
|
|
72
|
+
results = JSON.parse(stdout.trim());
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
console.error('Error: Could not parse browser-automation output');
|
|
76
|
+
console.error(stdout);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
// 10. Display results
|
|
80
|
+
if (options.json) {
|
|
81
|
+
outputJson(requirementTree, results);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
displayResults(requirementTree, results);
|
|
85
|
+
}
|
|
86
|
+
// 11. Exit with appropriate code
|
|
87
|
+
const failedCount = results.filter((r) => r.status !== 'passed').length;
|
|
88
|
+
if (failedCount > 0) {
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const error = err;
|
|
94
|
+
// browser-automation returns exit code 1 on failure, but still outputs JSON
|
|
95
|
+
if (error.stdout) {
|
|
96
|
+
try {
|
|
97
|
+
const results = JSON.parse(error.stdout.trim());
|
|
98
|
+
if (options.json) {
|
|
99
|
+
outputJson(requirementTree, results);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
displayResults(requirementTree, results);
|
|
103
|
+
}
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Not JSON, show raw output
|
|
108
|
+
console.error('browser-automation error:');
|
|
109
|
+
if (error.stdout)
|
|
110
|
+
console.error(error.stdout);
|
|
111
|
+
if (error.stderr)
|
|
112
|
+
console.error(error.stderr);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.error(`Error running browser-automation: ${error.message}`);
|
|
118
|
+
if (error.stderr)
|
|
119
|
+
console.error(error.stderr);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function displayResults(requirements, results) {
|
|
125
|
+
let passedCount = 0;
|
|
126
|
+
let failedCount = 0;
|
|
127
|
+
for (let i = 0; i < requirements.length; i++) {
|
|
128
|
+
const req = requirements[i];
|
|
129
|
+
const result = results[i];
|
|
130
|
+
if (!result) {
|
|
131
|
+
console.log(` ? ${req.id}: No result`);
|
|
132
|
+
failedCount++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const indent = ' '.repeat(req.path.length);
|
|
136
|
+
const label = req.label ? ` (${req.label})` : '';
|
|
137
|
+
const icon = result.status === 'passed' ? '\u2713' : '\u2717';
|
|
138
|
+
console.log(`${indent}${icon} ${req.id}${label}: ${req.content}`);
|
|
139
|
+
if (result.status !== 'passed' && result.notes) {
|
|
140
|
+
console.log(`${indent} \u2192 ${result.notes}`);
|
|
141
|
+
}
|
|
142
|
+
if (result.status === 'passed') {
|
|
143
|
+
passedCount++;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
failedCount++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
console.log();
|
|
150
|
+
if (failedCount === 0) {
|
|
151
|
+
console.log(`${passedCount}/${requirements.length} passed`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.log(`${passedCount}/${requirements.length} passed, ${failedCount} failed`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function outputJson(requirements, results) {
|
|
158
|
+
const combined = requirements.map((req, i) => ({
|
|
159
|
+
id: req.id,
|
|
160
|
+
label: req.label || null,
|
|
161
|
+
content: req.content,
|
|
162
|
+
status: results[i]?.status || 'blocked',
|
|
163
|
+
notes: results[i]?.notes || null,
|
|
164
|
+
}));
|
|
165
|
+
console.log(JSON.stringify(combined, null, 2));
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=browsertest.js.map
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser test configuration
|
|
3
|
+
*/
|
|
4
|
+
export interface BrowserTestSettings {
|
|
5
|
+
geminiApiKey?: string;
|
|
6
|
+
vercelBypassSecret?: string;
|
|
7
|
+
browserbaseApiKey?: string;
|
|
8
|
+
browserbaseProjectId?: string;
|
|
9
|
+
}
|
|
1
10
|
/**
|
|
2
11
|
* Project credentials stored in project-settings.json
|
|
3
12
|
*/
|
|
4
13
|
export interface ProjectSettings {
|
|
5
14
|
projectId: string;
|
|
6
15
|
projectSecret: string;
|
|
16
|
+
defaultURL?: string;
|
|
17
|
+
browserTest?: BrowserTestSettings;
|
|
7
18
|
}
|
|
8
19
|
/**
|
|
9
20
|
* Project status indicating whether it's connected to cloud
|
|
@@ -68,10 +68,32 @@ export function readProjectSettings(projectRoot) {
|
|
|
68
68
|
typeof parsed.projectSecret !== 'string') {
|
|
69
69
|
throw new Error('project-settings.json is invalid: missing projectId or projectSecret');
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const record = parsed;
|
|
72
|
+
const settings = {
|
|
73
|
+
projectId: record.projectId,
|
|
74
|
+
projectSecret: record.projectSecret,
|
|
74
75
|
};
|
|
76
|
+
// Optional fields
|
|
77
|
+
if (typeof record.defaultURL === 'string') {
|
|
78
|
+
settings.defaultURL = record.defaultURL;
|
|
79
|
+
}
|
|
80
|
+
if (typeof record.browserTest === 'object' && record.browserTest !== null) {
|
|
81
|
+
const bt = record.browserTest;
|
|
82
|
+
settings.browserTest = {};
|
|
83
|
+
if (typeof bt.geminiApiKey === 'string') {
|
|
84
|
+
settings.browserTest.geminiApiKey = bt.geminiApiKey;
|
|
85
|
+
}
|
|
86
|
+
if (typeof bt.vercelBypassSecret === 'string') {
|
|
87
|
+
settings.browserTest.vercelBypassSecret = bt.vercelBypassSecret;
|
|
88
|
+
}
|
|
89
|
+
if (typeof bt.browserbaseApiKey === 'string') {
|
|
90
|
+
settings.browserTest.browserbaseApiKey = bt.browserbaseApiKey;
|
|
91
|
+
}
|
|
92
|
+
if (typeof bt.browserbaseProjectId === 'string') {
|
|
93
|
+
settings.browserTest.browserbaseProjectId = bt.browserbaseProjectId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return settings;
|
|
75
97
|
}
|
|
76
98
|
/**
|
|
77
99
|
* Write project settings to .requirements/project-settings.json
|