@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.
- package/dist/adapters/claude-cli.d.ts +57 -0
- package/dist/adapters/claude-cli.d.ts.map +1 -0
- package/dist/adapters/claude-cli.js +195 -0
- package/dist/adapters/claude-cli.js.map +1 -0
- package/dist/adapters/claude.d.ts +54 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +160 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +196 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +3 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/agent-browser-session.d.ts +62 -0
- package/dist/agent-browser-session.d.ts.map +1 -0
- package/dist/agent-browser-session.js +272 -0
- package/dist/agent-browser-session.js.map +1 -0
- package/dist/evidence.d.ts +111 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +144 -0
- package/dist/evidence.js.map +1 -0
- package/dist/explorer.d.ts +180 -0
- package/dist/explorer.d.ts.map +1 -0
- package/dist/explorer.js +393 -0
- package/dist/explorer.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/locator-candidates.d.ts +127 -0
- package/dist/locator-candidates.d.ts.map +1 -0
- package/dist/locator-candidates.js +358 -0
- package/dist/locator-candidates.js.map +1 -0
- package/dist/step-executor.d.ts +99 -0
- package/dist/step-executor.d.ts.map +1 -0
- package/dist/step-executor.js +646 -0
- package/dist/step-executor.js.map +1 -0
- package/package.json +34 -0
- package/src/adapters/claude-cli.test.ts +134 -0
- package/src/adapters/claude-cli.ts +240 -0
- package/src/adapters/claude.test.ts +195 -0
- package/src/adapters/claude.ts +190 -0
- package/src/adapters/index.ts +21 -0
- package/src/adapters/types.ts +207 -0
- package/src/agent-browser-session.test.ts +369 -0
- package/src/agent-browser-session.ts +349 -0
- package/src/evidence.test.ts +239 -0
- package/src/evidence.ts +203 -0
- package/src/explorer.test.ts +321 -0
- package/src/explorer.ts +565 -0
- package/src/index.ts +51 -0
- package/src/locator-candidates.test.ts +602 -0
- package/src/locator-candidates.ts +441 -0
- package/src/step-executor.test.ts +696 -0
- package/src/step-executor.ts +783 -0
package/src/explorer.ts
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
// @browserflow-ai/exploration - Main exploration orchestrator
|
|
2
|
+
|
|
3
|
+
// Debug flag - set via BF_DEBUG=1 environment variable
|
|
4
|
+
const DEBUG = process.env.BF_DEBUG === '1';
|
|
5
|
+
|
|
6
|
+
function debug(...args: unknown[]): void {
|
|
7
|
+
if (DEBUG) {
|
|
8
|
+
console.error('[explorer]', ...args);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
AIAdapter,
|
|
14
|
+
ExploreParams,
|
|
15
|
+
ExplorationOutput,
|
|
16
|
+
Spec,
|
|
17
|
+
SpecStep,
|
|
18
|
+
StepResult,
|
|
19
|
+
StepExecution,
|
|
20
|
+
StepScreenshots,
|
|
21
|
+
EnhancedSnapshot,
|
|
22
|
+
} from './adapters/types';
|
|
23
|
+
import { StepExecutor } from './step-executor';
|
|
24
|
+
import { EvidenceCollector } from './evidence';
|
|
25
|
+
import { LocatorCandidateGenerator } from './locator-candidates';
|
|
26
|
+
import * as path from 'path';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Browser launch options
|
|
30
|
+
*/
|
|
31
|
+
export interface BrowserLaunchOptions {
|
|
32
|
+
headless?: boolean;
|
|
33
|
+
viewport?: { width: number; height: number };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Browser session interface - abstracts agent-browser integration
|
|
38
|
+
* Actual implementation will use agent-browser's BrowserManager
|
|
39
|
+
*/
|
|
40
|
+
export interface BrowserSession {
|
|
41
|
+
isLaunched(): boolean;
|
|
42
|
+
launch(options?: BrowserLaunchOptions): Promise<void>;
|
|
43
|
+
navigate(url: string): Promise<void>;
|
|
44
|
+
screenshot(options?: {
|
|
45
|
+
fullPage?: boolean;
|
|
46
|
+
clip?: { x: number; y: number; width: number; height: number };
|
|
47
|
+
mask?: string[];
|
|
48
|
+
quality?: number;
|
|
49
|
+
}): Promise<Buffer>;
|
|
50
|
+
getSnapshot(options?: {
|
|
51
|
+
interactive?: boolean;
|
|
52
|
+
maxDepth?: number;
|
|
53
|
+
compact?: boolean;
|
|
54
|
+
selector?: string;
|
|
55
|
+
}): Promise<EnhancedSnapshot>;
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
|
|
58
|
+
// Interaction methods
|
|
59
|
+
click?(ref: string): Promise<void>;
|
|
60
|
+
fill?(ref: string, value: string): Promise<void>;
|
|
61
|
+
type?(ref: string, text: string): Promise<void>;
|
|
62
|
+
select?(ref: string, option: string): Promise<void>;
|
|
63
|
+
check?(ref: string, checked: boolean): Promise<void>;
|
|
64
|
+
press?(key: string): Promise<void>;
|
|
65
|
+
|
|
66
|
+
// Navigation methods
|
|
67
|
+
back?(): Promise<void>;
|
|
68
|
+
forward?(): Promise<void>;
|
|
69
|
+
refresh?(): Promise<void>;
|
|
70
|
+
|
|
71
|
+
// Wait methods
|
|
72
|
+
waitForSelector?(selector: string, timeout: number): Promise<void>;
|
|
73
|
+
waitForURL?(urlPattern: string, timeout: number): Promise<void>;
|
|
74
|
+
waitForText?(text: string, timeout: number): Promise<void>;
|
|
75
|
+
waitForLoadState?(state: 'load' | 'domcontentloaded' | 'networkidle'): Promise<void>;
|
|
76
|
+
waitForTimeout?(ms: number): Promise<void>;
|
|
77
|
+
|
|
78
|
+
// Scroll methods
|
|
79
|
+
scrollIntoView?(ref: string): Promise<void>;
|
|
80
|
+
scroll?(x: number, y: number): Promise<void>;
|
|
81
|
+
|
|
82
|
+
// State methods
|
|
83
|
+
getCurrentURL?(): string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Configuration for the Explorer
|
|
88
|
+
*/
|
|
89
|
+
export interface ExplorerConfig {
|
|
90
|
+
adapter: AIAdapter;
|
|
91
|
+
browser?: BrowserSession;
|
|
92
|
+
outputDir?: string;
|
|
93
|
+
headless?: boolean;
|
|
94
|
+
viewport?: { width: number; height: number };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Explorer - Main orchestrator for AI-powered browser exploration
|
|
99
|
+
*
|
|
100
|
+
* Coordinates between:
|
|
101
|
+
* - AI adapter (Claude, OpenAI, etc.)
|
|
102
|
+
* - Step executor (runs individual steps)
|
|
103
|
+
* - Evidence collector (screenshots, traces)
|
|
104
|
+
* - Locator candidate generator (element selection strategies)
|
|
105
|
+
*/
|
|
106
|
+
export class Explorer {
|
|
107
|
+
private adapter: AIAdapter;
|
|
108
|
+
private browser?: BrowserSession;
|
|
109
|
+
private outputDir: string;
|
|
110
|
+
private headless: boolean;
|
|
111
|
+
private defaultViewport: { width: number; height: number };
|
|
112
|
+
private stepExecutor: StepExecutor;
|
|
113
|
+
private evidenceCollector: EvidenceCollector;
|
|
114
|
+
private locatorGenerator: LocatorCandidateGenerator;
|
|
115
|
+
|
|
116
|
+
constructor(config: ExplorerConfig) {
|
|
117
|
+
this.adapter = config.adapter;
|
|
118
|
+
this.browser = config.browser;
|
|
119
|
+
this.outputDir = config.outputDir ?? './explorations';
|
|
120
|
+
this.headless = config.headless ?? true;
|
|
121
|
+
this.defaultViewport = config.viewport ?? { width: 1280, height: 720 };
|
|
122
|
+
this.stepExecutor = new StepExecutor();
|
|
123
|
+
this.evidenceCollector = new EvidenceCollector();
|
|
124
|
+
this.locatorGenerator = new LocatorCandidateGenerator();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generate a unique exploration ID
|
|
129
|
+
*/
|
|
130
|
+
private generateExplorationId(): string {
|
|
131
|
+
const timestamp = Date.now();
|
|
132
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
133
|
+
return `exp-${timestamp}-${random}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run full exploration on a spec
|
|
138
|
+
*
|
|
139
|
+
* This is the main orchestration method that:
|
|
140
|
+
* 1. Launches browser
|
|
141
|
+
* 2. Navigates to starting page
|
|
142
|
+
* 3. Executes each step with evidence collection
|
|
143
|
+
* 4. Handles failures gracefully (continues on error)
|
|
144
|
+
* 5. Produces ExplorationOutput
|
|
145
|
+
*
|
|
146
|
+
* @param spec - The spec to explore
|
|
147
|
+
* @param baseUrl - Base URL for navigation
|
|
148
|
+
* @param options - Additional options
|
|
149
|
+
* @returns Promise resolving to exploration output
|
|
150
|
+
*/
|
|
151
|
+
async runExploration(
|
|
152
|
+
spec: Spec,
|
|
153
|
+
baseUrl: string,
|
|
154
|
+
options: Partial<{
|
|
155
|
+
specPath: string;
|
|
156
|
+
viewport: { width: number; height: number };
|
|
157
|
+
headless: boolean;
|
|
158
|
+
}> = {}
|
|
159
|
+
): Promise<ExplorationOutput> {
|
|
160
|
+
const startTime = Date.now();
|
|
161
|
+
const explorationId = this.generateExplorationId();
|
|
162
|
+
const steps: StepResult[] = [];
|
|
163
|
+
const errors: string[] = [];
|
|
164
|
+
|
|
165
|
+
// Configure evidence collector with exploration-specific output directory
|
|
166
|
+
const explorationOutputDir = path.join(this.outputDir, explorationId);
|
|
167
|
+
this.evidenceCollector.setOutputDir(explorationOutputDir);
|
|
168
|
+
|
|
169
|
+
// Determine viewport - spec preconditions override defaults
|
|
170
|
+
const viewport =
|
|
171
|
+
(spec.preconditions?.viewport as { width: number; height: number } | undefined) ??
|
|
172
|
+
options.viewport ??
|
|
173
|
+
this.defaultViewport;
|
|
174
|
+
|
|
175
|
+
// Ensure browser is available
|
|
176
|
+
if (!this.browser) {
|
|
177
|
+
throw new Error('Browser session not configured');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// 1. Launch browser
|
|
182
|
+
await this.browser.launch({
|
|
183
|
+
headless: options.headless ?? this.headless,
|
|
184
|
+
viewport,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Register browser session with evidence collector
|
|
188
|
+
this.evidenceCollector.registerSession(explorationId, this.browser);
|
|
189
|
+
|
|
190
|
+
// 2. Navigate to starting page
|
|
191
|
+
const pageConfig = spec.preconditions?.page as { url?: string } | undefined;
|
|
192
|
+
const startPage = pageConfig?.url ?? '/';
|
|
193
|
+
const fullUrl = startPage.startsWith('http') ? startPage : `${baseUrl}${startPage}`;
|
|
194
|
+
await this.browser.navigate(fullUrl);
|
|
195
|
+
|
|
196
|
+
// Wait briefly for JS to initialize (configurable via spec timeout)
|
|
197
|
+
// Note: For JS-heavy apps, consider adding explicit wait steps in the spec
|
|
198
|
+
if (this.browser.waitForTimeout) {
|
|
199
|
+
debug('Waiting 1s for page to stabilize...');
|
|
200
|
+
await this.browser.waitForTimeout(1000);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 3. Execute each step
|
|
204
|
+
for (let i = 0; i < spec.steps.length; i++) {
|
|
205
|
+
const specStep = spec.steps[i];
|
|
206
|
+
const stepResult = await this.executeStepWithEvidence(specStep, i, baseUrl, explorationId);
|
|
207
|
+
steps.push(stepResult);
|
|
208
|
+
|
|
209
|
+
if (stepResult.execution.status === 'failed' && stepResult.execution.error) {
|
|
210
|
+
errors.push(`Step ${i}: ${stepResult.execution.error}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 4. Build output
|
|
215
|
+
const overallStatus = steps.every((s) => s.execution.status === 'completed')
|
|
216
|
+
? 'completed'
|
|
217
|
+
: 'failed';
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
spec: spec.name,
|
|
221
|
+
specPath: options.specPath ?? `specs/${spec.name}.yaml`,
|
|
222
|
+
specDescription: spec.description,
|
|
223
|
+
explorationId,
|
|
224
|
+
timestamp: new Date().toISOString(),
|
|
225
|
+
durationMs: Date.now() - startTime,
|
|
226
|
+
browser: 'chromium',
|
|
227
|
+
viewport,
|
|
228
|
+
baseUrl,
|
|
229
|
+
steps,
|
|
230
|
+
outcomeChecks: [],
|
|
231
|
+
overallStatus,
|
|
232
|
+
errors,
|
|
233
|
+
};
|
|
234
|
+
} finally {
|
|
235
|
+
// Unregister browser session from evidence collector
|
|
236
|
+
this.evidenceCollector.unregisterSession(explorationId);
|
|
237
|
+
|
|
238
|
+
// Always close browser
|
|
239
|
+
await this.browser.close();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Execute a single step with evidence collection (screenshots)
|
|
245
|
+
*/
|
|
246
|
+
private async executeStepWithEvidence(
|
|
247
|
+
step: SpecStep,
|
|
248
|
+
stepIndex: number,
|
|
249
|
+
baseUrl: string,
|
|
250
|
+
explorationId: string
|
|
251
|
+
): Promise<StepResult> {
|
|
252
|
+
const startTime = Date.now();
|
|
253
|
+
let snapshotBefore: EnhancedSnapshot | undefined;
|
|
254
|
+
let snapshotAfter: EnhancedSnapshot | undefined;
|
|
255
|
+
|
|
256
|
+
// Capture before screenshot
|
|
257
|
+
const screenshotBeforePath = `screenshots/step-${String(stepIndex).padStart(2, '0')}-before.png`;
|
|
258
|
+
const screenshotAfterPath = `screenshots/step-${String(stepIndex).padStart(2, '0')}-after.png`;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
debug(`Step ${stepIndex}: ${step.action} - starting`);
|
|
262
|
+
|
|
263
|
+
// Take before screenshot and save to disk
|
|
264
|
+
debug(`Step ${stepIndex}: capturing before screenshot`);
|
|
265
|
+
await this.evidenceCollector.captureScreenshot(
|
|
266
|
+
explorationId,
|
|
267
|
+
`step-${String(stepIndex).padStart(2, '0')}-before`
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Get snapshot for element finding
|
|
271
|
+
debug(`Step ${stepIndex}: getting before snapshot`);
|
|
272
|
+
snapshotBefore = await this.browser!.getSnapshot({ interactive: true });
|
|
273
|
+
debug(`Step ${stepIndex}: snapshot has ${Object.keys(snapshotBefore.refs).length} refs, tree: ${snapshotBefore.tree.slice(0, 100)}...`);
|
|
274
|
+
|
|
275
|
+
// Execute the step
|
|
276
|
+
debug(`Step ${stepIndex}: executing action ${step.action}`);
|
|
277
|
+
const startAction = Date.now();
|
|
278
|
+
const execution = await this.executeAction(step, snapshotBefore, baseUrl);
|
|
279
|
+
debug(`Step ${stepIndex}: action completed in ${Date.now() - startAction}ms, status: ${execution.status}`);
|
|
280
|
+
|
|
281
|
+
// Take after screenshot and save to disk
|
|
282
|
+
debug(`Step ${stepIndex}: capturing after screenshot`);
|
|
283
|
+
await this.evidenceCollector.captureScreenshot(
|
|
284
|
+
explorationId,
|
|
285
|
+
`step-${String(stepIndex).padStart(2, '0')}-after`
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Get after snapshot
|
|
289
|
+
debug(`Step ${stepIndex}: getting after snapshot`);
|
|
290
|
+
snapshotAfter = await this.browser!.getSnapshot({ interactive: true });
|
|
291
|
+
debug(`Step ${stepIndex}: after snapshot has ${Object.keys(snapshotAfter.refs).length} refs`);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
stepIndex,
|
|
295
|
+
specAction: step as Record<string, unknown>,
|
|
296
|
+
execution: {
|
|
297
|
+
...execution,
|
|
298
|
+
durationMs: Date.now() - startTime,
|
|
299
|
+
},
|
|
300
|
+
screenshots: {
|
|
301
|
+
before: screenshotBeforePath,
|
|
302
|
+
after: screenshotAfterPath,
|
|
303
|
+
},
|
|
304
|
+
snapshotBefore: snapshotBefore as unknown as Record<string, unknown>,
|
|
305
|
+
snapshotAfter: snapshotAfter as unknown as Record<string, unknown>,
|
|
306
|
+
};
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
stepIndex,
|
|
312
|
+
specAction: step as Record<string, unknown>,
|
|
313
|
+
execution: {
|
|
314
|
+
status: 'failed',
|
|
315
|
+
method: step.action,
|
|
316
|
+
durationMs: Date.now() - startTime,
|
|
317
|
+
error: errorMessage,
|
|
318
|
+
},
|
|
319
|
+
screenshots: {
|
|
320
|
+
before: screenshotBeforePath,
|
|
321
|
+
after: screenshotAfterPath,
|
|
322
|
+
},
|
|
323
|
+
snapshotBefore: snapshotBefore as unknown as Record<string, unknown>,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Execute a single action based on step type
|
|
330
|
+
*/
|
|
331
|
+
private async executeAction(
|
|
332
|
+
step: SpecStep,
|
|
333
|
+
snapshot: EnhancedSnapshot,
|
|
334
|
+
baseUrl: string
|
|
335
|
+
): Promise<StepExecution> {
|
|
336
|
+
switch (step.action) {
|
|
337
|
+
case 'navigate': {
|
|
338
|
+
// Support both url (canonical) and to (legacy)
|
|
339
|
+
const targetUrl = step.url ?? step.to;
|
|
340
|
+
if (!targetUrl) {
|
|
341
|
+
throw new Error('Navigate action requires "url" field');
|
|
342
|
+
}
|
|
343
|
+
const url = targetUrl.startsWith('http') ? targetUrl : `${baseUrl}${targetUrl}`;
|
|
344
|
+
await this.browser!.navigate(url);
|
|
345
|
+
return {
|
|
346
|
+
status: 'completed',
|
|
347
|
+
method: 'navigate',
|
|
348
|
+
durationMs: 0,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
case 'click': {
|
|
353
|
+
const elementRef = await this.findElementRef(step, snapshot);
|
|
354
|
+
if (this.browser!.click) {
|
|
355
|
+
await this.browser!.click(elementRef);
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
status: 'completed',
|
|
359
|
+
method: 'click',
|
|
360
|
+
elementRef,
|
|
361
|
+
durationMs: 0,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
case 'fill': {
|
|
366
|
+
const elementRef = await this.findElementRef(step, snapshot);
|
|
367
|
+
const value = step.value ?? '';
|
|
368
|
+
if (this.browser!.fill) {
|
|
369
|
+
await this.browser!.fill(elementRef, value);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
status: 'completed',
|
|
373
|
+
method: 'fill',
|
|
374
|
+
elementRef,
|
|
375
|
+
durationMs: 0,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
case 'wait': {
|
|
380
|
+
// Wait actions are handled by timing, for now just mark completed
|
|
381
|
+
return {
|
|
382
|
+
status: 'completed',
|
|
383
|
+
method: 'wait',
|
|
384
|
+
durationMs: 0,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
case 'verify_state': {
|
|
389
|
+
// Verify state by checking conditions
|
|
390
|
+
return {
|
|
391
|
+
status: 'completed',
|
|
392
|
+
method: 'verify_state',
|
|
393
|
+
durationMs: 0,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
default: {
|
|
398
|
+
return {
|
|
399
|
+
status: 'completed',
|
|
400
|
+
method: step.action,
|
|
401
|
+
durationMs: 0,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Find element ref using AI adapter or direct selector/ref
|
|
409
|
+
*/
|
|
410
|
+
private async findElementRef(step: SpecStep, snapshot: EnhancedSnapshot): Promise<string> {
|
|
411
|
+
// If step has a direct ref, use it
|
|
412
|
+
if (step.ref) {
|
|
413
|
+
return step.ref;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// If step has a selector, it's already specified
|
|
417
|
+
if (step.selector) {
|
|
418
|
+
return step.selector;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Check for target object (v2 spec format)
|
|
422
|
+
const target = step.target as {
|
|
423
|
+
query?: string;
|
|
424
|
+
testid?: string;
|
|
425
|
+
role?: string;
|
|
426
|
+
css?: string;
|
|
427
|
+
text?: string;
|
|
428
|
+
label?: string;
|
|
429
|
+
placeholder?: string;
|
|
430
|
+
} | undefined;
|
|
431
|
+
|
|
432
|
+
if (target) {
|
|
433
|
+
// If target has CSS selector, use it directly
|
|
434
|
+
if (target.css) {
|
|
435
|
+
return target.css;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// If target has testid, convert to Playwright testId selector
|
|
439
|
+
if (target.testid) {
|
|
440
|
+
return `[data-testid="${target.testid}"]`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Use AI adapter to find element from query or other target properties
|
|
444
|
+
const query = target.query
|
|
445
|
+
|| (target.role && `a ${target.role} element`)
|
|
446
|
+
|| (target.text && `element with text "${target.text}"`)
|
|
447
|
+
|| (target.label && `element with label "${target.label}"`)
|
|
448
|
+
|| (target.placeholder && `input with placeholder "${target.placeholder}"`);
|
|
449
|
+
|
|
450
|
+
if (query) {
|
|
451
|
+
const result = await this.adapter.findElement(query, snapshot);
|
|
452
|
+
if (result.ref === 'NOT_FOUND') {
|
|
453
|
+
throw new Error(`Element not found for query: ${query}`);
|
|
454
|
+
}
|
|
455
|
+
return result.ref;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Legacy: Use AI adapter to find element from step.query (legacy format)
|
|
460
|
+
if ((step as { query?: string }).query) {
|
|
461
|
+
const result = await this.adapter.findElement((step as { query?: string }).query!, snapshot);
|
|
462
|
+
if (result.ref === 'NOT_FOUND') {
|
|
463
|
+
throw new Error(`Element not found for query: ${(step as { query?: string }).query}`);
|
|
464
|
+
}
|
|
465
|
+
return result.ref;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
throw new Error('Step must have ref, selector, target, or query');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Run exploration on a spec
|
|
473
|
+
*
|
|
474
|
+
* @param spec - The spec to explore
|
|
475
|
+
* @param baseUrl - Base URL for the browser session
|
|
476
|
+
* @param options - Additional options
|
|
477
|
+
* @returns Promise resolving to exploration output
|
|
478
|
+
*/
|
|
479
|
+
async explore(
|
|
480
|
+
spec: Spec,
|
|
481
|
+
baseUrl: string,
|
|
482
|
+
options: Partial<ExploreParams> = {}
|
|
483
|
+
): Promise<ExplorationOutput> {
|
|
484
|
+
const params: ExploreParams = {
|
|
485
|
+
spec,
|
|
486
|
+
specPath: options.specPath ?? `specs/${spec.name}.yaml`,
|
|
487
|
+
baseUrl,
|
|
488
|
+
browser: options.browser ?? 'chromium',
|
|
489
|
+
viewport: options.viewport ?? { width: 1280, height: 720 },
|
|
490
|
+
timeout: options.timeout ?? 30000,
|
|
491
|
+
outputDir: options.outputDir ?? `${this.outputDir}/${spec.name}`,
|
|
492
|
+
sessionId: options.sessionId,
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Delegate to adapter for AI-powered exploration
|
|
496
|
+
return this.adapter.explore(params);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Execute a single step manually (for testing/debugging)
|
|
501
|
+
*
|
|
502
|
+
* @param step - The step to execute
|
|
503
|
+
* @param stepIndex - Index of the step (default: 0)
|
|
504
|
+
* @returns Promise resolving to step result
|
|
505
|
+
*/
|
|
506
|
+
async executeStep(
|
|
507
|
+
step: Spec['steps'][number],
|
|
508
|
+
stepIndex: number = 0
|
|
509
|
+
): Promise<StepResult> {
|
|
510
|
+
return this.stepExecutor.execute(step, stepIndex);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Capture evidence (screenshot) at current state
|
|
515
|
+
*
|
|
516
|
+
* @param sessionId - Browser session ID
|
|
517
|
+
* @param name - Evidence name/identifier
|
|
518
|
+
* @returns Promise resolving to evidence file path
|
|
519
|
+
*/
|
|
520
|
+
async captureEvidence(sessionId: string, name: string): Promise<string> {
|
|
521
|
+
return this.evidenceCollector.captureScreenshot(sessionId, name);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Generate locator candidates for an element
|
|
526
|
+
*
|
|
527
|
+
* @param query - Natural language description of element
|
|
528
|
+
* @param snapshot - Browser snapshot with element refs
|
|
529
|
+
* @returns Promise resolving to ranked list of locator options
|
|
530
|
+
*/
|
|
531
|
+
async generateLocators(
|
|
532
|
+
query: string,
|
|
533
|
+
snapshot: Record<string, unknown>
|
|
534
|
+
): Promise<string[]> {
|
|
535
|
+
return this.locatorGenerator.generateCandidates(query, snapshot);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get the configured AI adapter
|
|
540
|
+
*/
|
|
541
|
+
getAdapter(): AIAdapter {
|
|
542
|
+
return this.adapter;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Get the step executor instance
|
|
547
|
+
*/
|
|
548
|
+
getStepExecutor(): StepExecutor {
|
|
549
|
+
return this.stepExecutor;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Get the evidence collector instance
|
|
554
|
+
*/
|
|
555
|
+
getEvidenceCollector(): EvidenceCollector {
|
|
556
|
+
return this.evidenceCollector;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Get the locator generator instance
|
|
561
|
+
*/
|
|
562
|
+
getLocatorGenerator(): LocatorCandidateGenerator {
|
|
563
|
+
return this.locatorGenerator;
|
|
564
|
+
}
|
|
565
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @browserflow-ai/exploration - AI exploration engine
|
|
2
|
+
|
|
3
|
+
// Main orchestrator
|
|
4
|
+
export { Explorer } from './explorer';
|
|
5
|
+
export type { ExplorerConfig, BrowserSession, BrowserLaunchOptions } from './explorer';
|
|
6
|
+
|
|
7
|
+
// Adapters
|
|
8
|
+
export { ClaudeAdapter } from './adapters/claude';
|
|
9
|
+
export type { ClaudeAdapterConfig } from './adapters/claude';
|
|
10
|
+
export { ClaudeCliAdapter } from './adapters/claude-cli';
|
|
11
|
+
export type { ClaudeCliAdapterConfig } from './adapters/claude-cli';
|
|
12
|
+
|
|
13
|
+
// Browser Session Adapters
|
|
14
|
+
export { AgentBrowserSession, createBrowserSession } from './agent-browser-session';
|
|
15
|
+
|
|
16
|
+
// Core types
|
|
17
|
+
export type {
|
|
18
|
+
AIAdapter,
|
|
19
|
+
ExploreParams,
|
|
20
|
+
ExplorationOutput,
|
|
21
|
+
RetryParams,
|
|
22
|
+
ReviewFeedback,
|
|
23
|
+
Spec,
|
|
24
|
+
SpecStep,
|
|
25
|
+
StepResult,
|
|
26
|
+
StepExecution,
|
|
27
|
+
StepScreenshots,
|
|
28
|
+
OutcomeCheck,
|
|
29
|
+
EnhancedSnapshot,
|
|
30
|
+
FindElementResult,
|
|
31
|
+
} from './adapters/types';
|
|
32
|
+
|
|
33
|
+
// Step execution
|
|
34
|
+
export { StepExecutor } from './step-executor';
|
|
35
|
+
export type { StepExecutorConfig } from './step-executor';
|
|
36
|
+
|
|
37
|
+
// Evidence collection
|
|
38
|
+
export { EvidenceCollector } from './evidence';
|
|
39
|
+
export type {
|
|
40
|
+
EvidenceCollectorConfig,
|
|
41
|
+
EvidenceMetadata,
|
|
42
|
+
ScreenshotOptions,
|
|
43
|
+
} from './evidence';
|
|
44
|
+
|
|
45
|
+
// Locator generation
|
|
46
|
+
export { LocatorCandidateGenerator } from './locator-candidates';
|
|
47
|
+
export type {
|
|
48
|
+
LocatorCandidateGeneratorConfig,
|
|
49
|
+
LocatorCandidate,
|
|
50
|
+
ElementInfo,
|
|
51
|
+
} from './locator-candidates';
|