@browserflow-ai/exploration 0.0.6

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.
Files changed (58) hide show
  1. package/dist/adapters/claude-cli.d.ts +57 -0
  2. package/dist/adapters/claude-cli.d.ts.map +1 -0
  3. package/dist/adapters/claude-cli.js +195 -0
  4. package/dist/adapters/claude-cli.js.map +1 -0
  5. package/dist/adapters/claude.d.ts +54 -0
  6. package/dist/adapters/claude.d.ts.map +1 -0
  7. package/dist/adapters/claude.js +160 -0
  8. package/dist/adapters/claude.js.map +1 -0
  9. package/dist/adapters/index.d.ts +6 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters/index.js +4 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/types.d.ts +196 -0
  14. package/dist/adapters/types.d.ts.map +1 -0
  15. package/dist/adapters/types.js +3 -0
  16. package/dist/adapters/types.js.map +1 -0
  17. package/dist/agent-browser-session.d.ts +62 -0
  18. package/dist/agent-browser-session.d.ts.map +1 -0
  19. package/dist/agent-browser-session.js +272 -0
  20. package/dist/agent-browser-session.js.map +1 -0
  21. package/dist/evidence.d.ts +111 -0
  22. package/dist/evidence.d.ts.map +1 -0
  23. package/dist/evidence.js +144 -0
  24. package/dist/evidence.js.map +1 -0
  25. package/dist/explorer.d.ts +180 -0
  26. package/dist/explorer.d.ts.map +1 -0
  27. package/dist/explorer.js +393 -0
  28. package/dist/explorer.js.map +1 -0
  29. package/dist/index.d.ts +15 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +15 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/locator-candidates.d.ts +127 -0
  34. package/dist/locator-candidates.d.ts.map +1 -0
  35. package/dist/locator-candidates.js +358 -0
  36. package/dist/locator-candidates.js.map +1 -0
  37. package/dist/step-executor.d.ts +99 -0
  38. package/dist/step-executor.d.ts.map +1 -0
  39. package/dist/step-executor.js +646 -0
  40. package/dist/step-executor.js.map +1 -0
  41. package/package.json +34 -0
  42. package/src/adapters/claude-cli.test.ts +134 -0
  43. package/src/adapters/claude-cli.ts +240 -0
  44. package/src/adapters/claude.test.ts +195 -0
  45. package/src/adapters/claude.ts +190 -0
  46. package/src/adapters/index.ts +21 -0
  47. package/src/adapters/types.ts +207 -0
  48. package/src/agent-browser-session.test.ts +369 -0
  49. package/src/agent-browser-session.ts +349 -0
  50. package/src/evidence.test.ts +239 -0
  51. package/src/evidence.ts +203 -0
  52. package/src/explorer.test.ts +321 -0
  53. package/src/explorer.ts +565 -0
  54. package/src/index.ts +51 -0
  55. package/src/locator-candidates.test.ts +602 -0
  56. package/src/locator-candidates.ts +441 -0
  57. package/src/step-executor.test.ts +696 -0
  58. package/src/step-executor.ts +783 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Tests for EvidenceCollector - Screenshot and trace evidence capture
3
+ */
4
+
5
+ import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
6
+ import { promises as fs } from 'fs';
7
+ import * as path from 'path';
8
+ import { EvidenceCollector } from './evidence';
9
+ import type { BrowserSession } from './explorer';
10
+
11
+ describe('EvidenceCollector', () => {
12
+ let collector: EvidenceCollector;
13
+ let mockSession: BrowserSession;
14
+ let tempDir: string;
15
+
16
+ beforeEach(async () => {
17
+ // Create temp directory for test outputs
18
+ tempDir = `/tmp/evidence-test-${Date.now()}`;
19
+ await fs.mkdir(tempDir, { recursive: true });
20
+
21
+ collector = new EvidenceCollector({
22
+ outputDir: tempDir,
23
+ screenshotFormat: 'png',
24
+ });
25
+
26
+ // Mock browser session
27
+ mockSession = {
28
+ isLaunched: () => true,
29
+ launch: mock(async () => {}),
30
+ navigate: mock(async () => {}),
31
+ screenshot: mock(async () => Buffer.from('fake-png-data')),
32
+ getSnapshot: mock(async () => ({ refs: {}, elements: [] })),
33
+ close: mock(async () => {}),
34
+ };
35
+ });
36
+
37
+ afterEach(async () => {
38
+ // Clean up temp directory
39
+ try {
40
+ await fs.rm(tempDir, { recursive: true, force: true });
41
+ } catch {
42
+ // Ignore cleanup errors
43
+ }
44
+ });
45
+
46
+ describe('Session Management', () => {
47
+ test('registerSession stores a browser session', () => {
48
+ collector.registerSession('test-session-1', mockSession);
49
+ // If this doesn't throw, registration worked
50
+ expect(true).toBe(true);
51
+ });
52
+
53
+ test('registerSession allows multiple sessions', () => {
54
+ const mockSession2: BrowserSession = {
55
+ ...mockSession,
56
+ screenshot: mock(async () => Buffer.from('fake-png-data-2')),
57
+ };
58
+
59
+ collector.registerSession('session-1', mockSession);
60
+ collector.registerSession('session-2', mockSession2);
61
+ // Should not throw
62
+ expect(true).toBe(true);
63
+ });
64
+
65
+ test('unregisterSession removes a session', () => {
66
+ collector.registerSession('test-session', mockSession);
67
+ collector.unregisterSession('test-session');
68
+ // Should not throw
69
+ expect(true).toBe(true);
70
+ });
71
+ });
72
+
73
+ describe('Screenshot Capture', () => {
74
+ beforeEach(() => {
75
+ collector.registerSession('test-session', mockSession);
76
+ });
77
+
78
+ test('captureScreenshot creates screenshots directory', async () => {
79
+ const filepath = await collector.captureScreenshot('test-session', 'test-screenshot');
80
+
81
+ const screenshotsDir = path.join(tempDir, 'screenshots');
82
+ const dirExists = await fs
83
+ .stat(screenshotsDir)
84
+ .then((s) => s.isDirectory())
85
+ .catch(() => false);
86
+
87
+ expect(dirExists).toBe(true);
88
+ });
89
+
90
+ test('captureScreenshot writes file to disk', async () => {
91
+ const filepath = await collector.captureScreenshot('test-session', 'test-screenshot');
92
+
93
+ const fileExists = await fs
94
+ .stat(filepath)
95
+ .then(() => true)
96
+ .catch(() => false);
97
+
98
+ expect(fileExists).toBe(true);
99
+ });
100
+
101
+ test('captureScreenshot returns correct file path', async () => {
102
+ const filepath = await collector.captureScreenshot('test-session', 'my-screenshot');
103
+
104
+ const expectedPath = path.join(tempDir, 'screenshots', 'my-screenshot.png');
105
+ expect(filepath).toBe(expectedPath);
106
+ });
107
+
108
+ test('captureScreenshot writes actual screenshot buffer', async () => {
109
+ const testBuffer = Buffer.from('test-screenshot-data');
110
+ mockSession.screenshot = mock(async () => testBuffer);
111
+
112
+ const filepath = await collector.captureScreenshot('test-session', 'test-screenshot');
113
+
114
+ const fileContent = await fs.readFile(filepath);
115
+ expect(fileContent).toEqual(testBuffer);
116
+ });
117
+
118
+ test('captureScreenshot records metadata', async () => {
119
+ await collector.captureScreenshot('test-session', 'test-screenshot');
120
+
121
+ const evidence = collector.getEvidence();
122
+ expect(evidence.length).toBe(1);
123
+ expect(evidence[0].type).toBe('screenshot');
124
+ expect(evidence[0].sessionId).toBe('test-session');
125
+ expect(evidence[0].path).toContain('test-screenshot.png');
126
+ });
127
+
128
+ test('captureScreenshot throws error for unknown session', async () => {
129
+ await expect(
130
+ collector.captureScreenshot('unknown-session', 'test')
131
+ ).rejects.toThrow('No browser session found: unknown-session');
132
+ });
133
+
134
+ test('captureScreenshot respects screenshot format (jpeg)', async () => {
135
+ const jpegCollector = new EvidenceCollector({
136
+ outputDir: tempDir,
137
+ screenshotFormat: 'jpeg',
138
+ });
139
+ jpegCollector.registerSession('test-session', mockSession);
140
+
141
+ const filepath = await jpegCollector.captureScreenshot('test-session', 'test-jpeg');
142
+
143
+ expect(filepath).toContain('.jpeg');
144
+ });
145
+
146
+ test('captureScreenshot passes options to browser session', async () => {
147
+ const screenshotMock = mock(async () => Buffer.from('data'));
148
+ mockSession.screenshot = screenshotMock;
149
+
150
+ const options = {
151
+ fullPage: true,
152
+ clip: { x: 0, y: 0, width: 100, height: 100 },
153
+ };
154
+
155
+ await collector.captureScreenshot('test-session', 'test', options);
156
+
157
+ expect(screenshotMock).toHaveBeenCalledWith(options);
158
+ });
159
+
160
+ test('captureScreenshot handles multiple screenshots', async () => {
161
+ await collector.captureScreenshot('test-session', 'screenshot-1');
162
+ await collector.captureScreenshot('test-session', 'screenshot-2');
163
+ await collector.captureScreenshot('test-session', 'screenshot-3');
164
+
165
+ const evidence = collector.getEvidence();
166
+ expect(evidence.length).toBe(3);
167
+ expect(evidence[0].path).toContain('screenshot-1');
168
+ expect(evidence[1].path).toContain('screenshot-2');
169
+ expect(evidence[2].path).toContain('screenshot-3');
170
+ });
171
+
172
+ test('captureScreenshot creates nested directories if needed', async () => {
173
+ // Even if screenshots dir doesn't exist, it should be created
174
+ const screenshotsDir = path.join(tempDir, 'screenshots');
175
+ const dirExistsBefore = await fs
176
+ .stat(screenshotsDir)
177
+ .then(() => true)
178
+ .catch(() => false);
179
+
180
+ expect(dirExistsBefore).toBe(false);
181
+
182
+ await collector.captureScreenshot('test-session', 'test');
183
+
184
+ const dirExistsAfter = await fs
185
+ .stat(screenshotsDir)
186
+ .then((s) => s.isDirectory())
187
+ .catch(() => false);
188
+
189
+ expect(dirExistsAfter).toBe(true);
190
+ });
191
+ });
192
+
193
+ describe('Evidence Metadata', () => {
194
+ beforeEach(() => {
195
+ collector.registerSession('test-session', mockSession);
196
+ });
197
+
198
+ test('getEvidence returns all captured evidence', async () => {
199
+ await collector.captureScreenshot('test-session', 'shot-1');
200
+ await collector.captureScreenshot('test-session', 'shot-2');
201
+
202
+ const evidence = collector.getEvidence();
203
+ expect(evidence.length).toBe(2);
204
+ });
205
+
206
+ test('clearEvidence removes all metadata', async () => {
207
+ await collector.captureScreenshot('test-session', 'shot-1');
208
+ await collector.captureScreenshot('test-session', 'shot-2');
209
+
210
+ collector.clearEvidence();
211
+
212
+ const evidence = collector.getEvidence();
213
+ expect(evidence.length).toBe(0);
214
+ });
215
+
216
+ test('evidence metadata includes timestamp', async () => {
217
+ const beforeTime = new Date().toISOString();
218
+ await collector.captureScreenshot('test-session', 'test');
219
+ const afterTime = new Date().toISOString();
220
+
221
+ const evidence = collector.getEvidence();
222
+ expect(evidence[0].timestamp).toBeDefined();
223
+ expect(evidence[0].timestamp >= beforeTime).toBe(true);
224
+ expect(evidence[0].timestamp <= afterTime).toBe(true);
225
+ });
226
+ });
227
+
228
+ describe('Configuration', () => {
229
+ test('getOutputDir returns configured directory', () => {
230
+ expect(collector.getOutputDir()).toBe(tempDir);
231
+ });
232
+
233
+ test('setOutputDir updates directory', () => {
234
+ const newDir = '/tmp/new-dir';
235
+ collector.setOutputDir(newDir);
236
+ expect(collector.getOutputDir()).toBe(newDir);
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,203 @@
1
+ // @browserflow-ai/exploration - Evidence collection (screenshots and traces)
2
+
3
+ import { promises as fs } from 'fs';
4
+ import * as path from 'path';
5
+ import type { BrowserSession } from './explorer';
6
+
7
+ /**
8
+ * Evidence metadata
9
+ */
10
+ export interface EvidenceMetadata {
11
+ timestamp: string;
12
+ sessionId: string;
13
+ stepIndex?: number;
14
+ type: 'screenshot' | 'trace' | 'snapshot';
15
+ path: string;
16
+ }
17
+
18
+ /**
19
+ * Screenshot capture options
20
+ */
21
+ export interface ScreenshotOptions {
22
+ fullPage?: boolean;
23
+ clip?: { x: number; y: number; width: number; height: number };
24
+ mask?: string[];
25
+ quality?: number;
26
+ }
27
+
28
+ /**
29
+ * Configuration for the evidence collector
30
+ */
31
+ export interface EvidenceCollectorConfig {
32
+ outputDir?: string;
33
+ screenshotFormat?: 'png' | 'jpeg';
34
+ screenshotQuality?: number;
35
+ }
36
+
37
+ /**
38
+ * EvidenceCollector - Captures screenshots and traces during exploration
39
+ *
40
+ * Provides:
41
+ * - Screenshot capture (full page, clipped, masked)
42
+ * - Browser snapshot capture
43
+ * - Trace/HAR file generation
44
+ * - Evidence metadata tracking
45
+ */
46
+ export class EvidenceCollector {
47
+ private outputDir: string;
48
+ private screenshotFormat: 'png' | 'jpeg';
49
+ private screenshotQuality: number;
50
+ private evidence: EvidenceMetadata[] = [];
51
+ private sessions: Map<string, BrowserSession> = new Map();
52
+
53
+ constructor(config: EvidenceCollectorConfig = {}) {
54
+ this.outputDir = config.outputDir ?? './evidence';
55
+ this.screenshotFormat = config.screenshotFormat ?? 'png';
56
+ this.screenshotQuality = config.screenshotQuality ?? 90;
57
+ }
58
+
59
+ /**
60
+ * Register a browser session for screenshot capture
61
+ *
62
+ * @param sessionId - Unique identifier for the session
63
+ * @param session - Browser session instance
64
+ */
65
+ registerSession(sessionId: string, session: BrowserSession): void {
66
+ this.sessions.set(sessionId, session);
67
+ }
68
+
69
+ /**
70
+ * Unregister a browser session
71
+ *
72
+ * @param sessionId - Session identifier to remove
73
+ */
74
+ unregisterSession(sessionId: string): void {
75
+ this.sessions.delete(sessionId);
76
+ }
77
+
78
+ /**
79
+ * Capture a screenshot from the browser session
80
+ *
81
+ * @param sessionId - Browser session ID
82
+ * @param name - Screenshot name/identifier
83
+ * @param options - Screenshot options
84
+ * @returns Promise resolving to screenshot file path
85
+ */
86
+ async captureScreenshot(
87
+ sessionId: string,
88
+ name: string,
89
+ options: ScreenshotOptions = {}
90
+ ): Promise<string> {
91
+ // Get browser session
92
+ const session = this.sessions.get(sessionId);
93
+ if (!session) {
94
+ throw new Error(`No browser session found: ${sessionId}`);
95
+ }
96
+
97
+ // Build file path
98
+ const filename = `${name}.${this.screenshotFormat}`;
99
+ const filepath = path.join(this.outputDir, 'screenshots', filename);
100
+
101
+ // Ensure directory exists
102
+ await fs.mkdir(path.dirname(filepath), { recursive: true });
103
+
104
+ // Capture screenshot from browser session
105
+ const buffer = await session.screenshot(options);
106
+ await fs.writeFile(filepath, buffer);
107
+
108
+ // Record metadata
109
+ const metadata: EvidenceMetadata = {
110
+ timestamp: new Date().toISOString(),
111
+ sessionId,
112
+ type: 'screenshot',
113
+ path: filepath,
114
+ };
115
+
116
+ this.evidence.push(metadata);
117
+ return filepath;
118
+ }
119
+
120
+ /**
121
+ * Capture a browser snapshot (DOM state with element refs)
122
+ *
123
+ * @param sessionId - Browser session ID
124
+ * @param name - Snapshot name/identifier
125
+ * @returns Promise resolving to snapshot data
126
+ */
127
+ async captureSnapshot(
128
+ sessionId: string,
129
+ name: string
130
+ ): Promise<Record<string, unknown>> {
131
+ // TODO: Implement actual snapshot capture via agent-browser
132
+ const path = `${this.outputDir}/snapshots/${name}.json`;
133
+
134
+ const metadata: EvidenceMetadata = {
135
+ timestamp: new Date().toISOString(),
136
+ sessionId,
137
+ type: 'snapshot',
138
+ path,
139
+ };
140
+
141
+ this.evidence.push(metadata);
142
+ return { elements: [], path };
143
+ }
144
+
145
+ /**
146
+ * Start trace recording
147
+ *
148
+ * @param sessionId - Browser session ID
149
+ * @param name - Trace name/identifier
150
+ */
151
+ async startTrace(sessionId: string, name: string): Promise<void> {
152
+ // TODO: Implement trace recording via agent-browser
153
+ }
154
+
155
+ /**
156
+ * Stop trace recording and save
157
+ *
158
+ * @param sessionId - Browser session ID
159
+ * @returns Promise resolving to trace file path
160
+ */
161
+ async stopTrace(sessionId: string): Promise<string> {
162
+ // TODO: Implement trace recording via agent-browser
163
+ const path = `${this.outputDir}/traces/trace.zip`;
164
+
165
+ const metadata: EvidenceMetadata = {
166
+ timestamp: new Date().toISOString(),
167
+ sessionId,
168
+ type: 'trace',
169
+ path,
170
+ };
171
+
172
+ this.evidence.push(metadata);
173
+ return path;
174
+ }
175
+
176
+ /**
177
+ * Get all collected evidence metadata
178
+ */
179
+ getEvidence(): EvidenceMetadata[] {
180
+ return [...this.evidence];
181
+ }
182
+
183
+ /**
184
+ * Clear collected evidence metadata
185
+ */
186
+ clearEvidence(): void {
187
+ this.evidence = [];
188
+ }
189
+
190
+ /**
191
+ * Get the output directory
192
+ */
193
+ getOutputDir(): string {
194
+ return this.outputDir;
195
+ }
196
+
197
+ /**
198
+ * Set the output directory
199
+ */
200
+ setOutputDir(dir: string): void {
201
+ this.outputDir = dir;
202
+ }
203
+ }