@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 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,6 @@
1
+ interface BrowserTestOptions {
2
+ json?: boolean;
3
+ }
4
+ export declare function browserTestCommand(requirementKey: string, url: string | undefined, options: BrowserTestOptions): Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=browsertest.d.ts.map
@@ -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
- return {
72
- projectId: parsed.projectId,
73
- projectSecret: parsed.projectSecret,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Requirements tracking CLI, test harness, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {