explorbot 0.2.3 → 0.2.5
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 +1 -1
- package/bin/explorbot-cli.ts +26 -8
- package/boat/api-tester/src/cli.ts +17 -0
- package/boat/api-tester/src/config.ts +4 -2
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +14 -1
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +0 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +127 -86
- package/boat/prima/src/envelope.ts +102 -52
- package/boat/prima/src/prima.ts +567 -128
- package/boat/prima/src/pw-parser.ts +11 -1
- package/boat/prima/src/pw-registry.ts +4 -5
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +26 -8
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/cli.js +17 -0
- package/dist/boat/api-tester/src/config.js +4 -2
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +14 -1
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +109 -77
- package/dist/boat/prima/src/envelope.js +94 -44
- package/dist/boat/prima/src/prima.js +533 -119
- package/dist/boat/prima/src/pw-parser.js +13 -1
- package/dist/boat/prima/src/pw-registry.js +4 -5
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/package.json +3 -2
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +2 -0
- package/dist/src/action.js +41 -2
- package/dist/src/ai/captain/web-mode.js +6 -3
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +34 -0
- package/dist/src/ai/navigator.js +237 -181
- package/dist/src/ai/pilot.d.ts +7 -0
- package/dist/src/ai/pilot.js +90 -2
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +14 -23
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher/cache.d.ts +2 -0
- package/dist/src/ai/researcher/cache.js +10 -2
- package/dist/src/ai/researcher.js +3 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/task-agent.js +4 -1
- package/dist/src/ai/tester.d.ts +6 -3
- package/dist/src/ai/tester.js +50 -46
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +117 -37
- package/dist/src/commands/config-command.d.ts +51 -0
- package/dist/src/commands/config-command.js +117 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/config.d.ts +9 -1
- package/dist/src/config.js +53 -4
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +2 -1
- package/dist/src/explorbot.js +7 -2
- package/dist/src/explorer.js +2 -3
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +55 -0
- package/dist/src/remote.js +235 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/state-manager.js +3 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +26 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +7 -1
- package/dist/src/utils/logger.js +32 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/url-matcher.js +3 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +87 -0
- package/docs/reference/commands.md +997 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/reference/websocket.md +50 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/package.json +3 -2
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +43 -3
- package/src/ai/captain/web-mode.ts +6 -3
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +255 -186
- package/src/ai/pilot.ts +104 -2
- package/src/ai/provider.ts +14 -24
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher/cache.ts +12 -2
- package/src/ai/researcher.ts +3 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/task-agent.ts +3 -1
- package/src/ai/tester.ts +52 -45
- package/src/ai/tools.ts +136 -37
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/config.ts +60 -5
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +7 -3
- package/src/explorer.ts +2 -2
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +244 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +6 -2
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +33 -2
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/url-matcher.ts +3 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
package/src/state-manager.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ActionResult } from './action-result.js';
|
|
1
|
+
import { ActionResult, type FocusedElement } from './action-result.js';
|
|
2
2
|
import type { ExperienceTracker } from './experience-tracker.js';
|
|
3
3
|
import type { Knowledge, KnowledgeTracker } from './knowledge-tracker.js';
|
|
4
4
|
import { detectFocusArea } from './utils/aria.js';
|
|
5
|
-
import { createDebug } from './utils/logger.js';
|
|
5
|
+
import { createDebug, tag } from './utils/logger.js';
|
|
6
6
|
import { slugify } from './utils/strings.js';
|
|
7
7
|
import { extractStatePath } from './utils/url-matcher.js';
|
|
8
8
|
|
|
@@ -46,6 +46,7 @@ export interface WebPageState {
|
|
|
46
46
|
h4?: string;
|
|
47
47
|
ariaSnapshot?: string | null;
|
|
48
48
|
ariaSnapshotFile?: string;
|
|
49
|
+
focusedElement?: FocusedElement | null;
|
|
49
50
|
links?: Link[];
|
|
50
51
|
verifications?: Record<string, boolean>;
|
|
51
52
|
}
|
|
@@ -116,6 +117,9 @@ export class StateManager {
|
|
|
116
117
|
* Emit state change event to all listeners
|
|
117
118
|
*/
|
|
118
119
|
private emitStateChange(event: StateTransition): void {
|
|
120
|
+
const state = event.toState;
|
|
121
|
+
tag('data').log('state', { url: state.fullUrl || state.url, path: state.url, title: state.title, h1: state.h1 });
|
|
122
|
+
|
|
119
123
|
this.stateChangeListeners.forEach((listener) => {
|
|
120
124
|
try {
|
|
121
125
|
listener(event);
|
package/src/stats.ts
CHANGED
|
@@ -17,6 +17,7 @@ export class Stats {
|
|
|
17
17
|
static plans = 0;
|
|
18
18
|
static mode?: ExplorbotMode;
|
|
19
19
|
static focus?: string;
|
|
20
|
+
static visionDisabled = false;
|
|
20
21
|
static models: Record<string, TokenUsage> = {};
|
|
21
22
|
|
|
22
23
|
static recordTokens(_agent: string, model: string, usage: TokenUsage): void {
|
package/src/test-plan.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import figures from 'figures';
|
|
3
3
|
import { WebPageState } from './state-manager.ts';
|
|
4
|
+
import { tag } from './utils/logger.ts';
|
|
4
5
|
import { parsePlanFromMarkdown, planToAiContext, savePlanToMarkdown, savePlansToMarkdown } from './utils/test-plan-markdown.ts';
|
|
5
6
|
import { uniqSessionName } from './utils/unique-names.ts';
|
|
6
7
|
|
|
@@ -27,6 +28,7 @@ export interface Note {
|
|
|
27
28
|
endTime: number;
|
|
28
29
|
screenshot?: string;
|
|
29
30
|
log?: string;
|
|
31
|
+
observation?: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export class ActiveNote {
|
|
@@ -131,6 +133,14 @@ export class Task {
|
|
|
131
133
|
this.notes[timestamp] = { message, status, startTime: now, endTime: now, screenshot, log };
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
addObservation(message: string): void {
|
|
137
|
+
const isDuplicate = Object.values(this.notes).some((note) => note.message === message);
|
|
138
|
+
if (isDuplicate) return;
|
|
139
|
+
|
|
140
|
+
const now = performance.now();
|
|
141
|
+
this.notes[`${now}_${this.timestampCounter++}`] = { message, status: TestResult.FAILED, startTime: now, endTime: now, observation: true };
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
addUrlNote(state: UrlNoteState, prevState?: { title?: string; h1?: string; h2?: string }): void {
|
|
135
145
|
const fullUrl = state.fullUrl || state.url;
|
|
136
146
|
if (!fullUrl) return;
|
|
@@ -308,6 +318,7 @@ export class Test extends Task {
|
|
|
308
318
|
this.startTime = performance.now();
|
|
309
319
|
this.addNote(`Test started. Session name: ${this.sessionName}`);
|
|
310
320
|
this.plan?.notifyChange();
|
|
321
|
+
this.reportStatus();
|
|
311
322
|
}
|
|
312
323
|
|
|
313
324
|
finish(result: TestResultType = TestResult.FAILED): void {
|
|
@@ -315,6 +326,7 @@ export class Test extends Task {
|
|
|
315
326
|
this.result = result;
|
|
316
327
|
this.endTime = performance.now();
|
|
317
328
|
this.plan?.notifyChange();
|
|
329
|
+
this.reportStatus();
|
|
318
330
|
}
|
|
319
331
|
|
|
320
332
|
getDurationMs(): number | null {
|
|
@@ -327,6 +339,18 @@ export class Test extends Task {
|
|
|
327
339
|
return this.expected.filter((e) => !achieved.includes(e));
|
|
328
340
|
}
|
|
329
341
|
|
|
342
|
+
private reportStatus(): void {
|
|
343
|
+
tag('data').log('test', {
|
|
344
|
+
scenario: this.scenario,
|
|
345
|
+
status: this.status,
|
|
346
|
+
result: this.result,
|
|
347
|
+
priority: this.priority,
|
|
348
|
+
sessionName: this.sessionName,
|
|
349
|
+
url: this.startUrl,
|
|
350
|
+
plan: this.plan?.title,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
330
354
|
override getLog(): Array<{ type: 'step' | 'note' | 'artifact'; content: string; timestamp: number }> {
|
|
331
355
|
const merged: Record<string, { type: 'step' | 'note' | 'artifact'; content: string }> = {};
|
|
332
356
|
|
|
@@ -398,6 +422,11 @@ export class Plan {
|
|
|
398
422
|
for (const listener of this.changeListeners) {
|
|
399
423
|
listener(this.tests);
|
|
400
424
|
}
|
|
425
|
+
tag('data').log('plan', {
|
|
426
|
+
title: this.title,
|
|
427
|
+
url: this.url,
|
|
428
|
+
tests: this.tests.map((test) => ({ scenario: test.scenario, status: test.status, result: test.result, priority: test.priority })),
|
|
429
|
+
});
|
|
401
430
|
}
|
|
402
431
|
|
|
403
432
|
getAllTests(): Test[] {
|
package/src/utils/aria.ts
CHANGED
|
@@ -85,8 +85,7 @@ const parseLabel = (label: string): { role: string; name?: string; attributes: R
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const attributes: Record<string, string | boolean | null> = {};
|
|
88
|
-
const attrMatch
|
|
89
|
-
if (attrMatch) {
|
|
88
|
+
for (const attrMatch of rest.matchAll(/\[([^\]]*)\]/g)) {
|
|
90
89
|
for (const tok of attrMatch[1].split(/[\s,]+/).filter(Boolean)) {
|
|
91
90
|
const eq = tok.indexOf('=');
|
|
92
91
|
if (eq === -1) {
|
|
@@ -192,7 +191,9 @@ const dropEmpty = (nodes: AriaNode[], opts: { keepNamed?: boolean } = {}): AriaN
|
|
|
192
191
|
// ─────────────────────────────────────────────────────────────────
|
|
193
192
|
|
|
194
193
|
// One-line representation of a node. Stable attr order so diff comparisons are deterministic.
|
|
195
|
-
const
|
|
194
|
+
const MAX_INLINE_VALUE = 400;
|
|
195
|
+
|
|
196
|
+
const formatNode = (node: AriaNode, offload?: ValueOffload): string => {
|
|
196
197
|
let line = node.role;
|
|
197
198
|
if (node.name?.trim()) line += ` "${node.name.trim()}"`;
|
|
198
199
|
const attrStr = Object.keys(node.attributes)
|
|
@@ -208,11 +209,19 @@ const formatNode = (node: AriaNode): string => {
|
|
|
208
209
|
if (attrStr) line += ` [${attrStr}]`;
|
|
209
210
|
if (node.value !== undefined && node.value !== null) {
|
|
210
211
|
const text = String(node.value).trim();
|
|
211
|
-
if (text) line += `: ${text}`;
|
|
212
|
+
if (text && text.length <= MAX_INLINE_VALUE) line += `: ${text}`;
|
|
213
|
+
if (text && text.length > MAX_INLINE_VALUE) line += `: ${offloadValue(text, offload)}`;
|
|
212
214
|
}
|
|
213
215
|
return line;
|
|
214
216
|
};
|
|
215
217
|
|
|
218
|
+
const offloadValue = (text: string, offload?: ValueOffload): string => {
|
|
219
|
+
const head = `${text.slice(0, MAX_INLINE_VALUE)}…`;
|
|
220
|
+
const reference = offload?.(text);
|
|
221
|
+
if (!reference) return `${head} (${text.length} chars, truncated)`;
|
|
222
|
+
return `${head} (${text.length} chars) [Full Text: ${reference}]`;
|
|
223
|
+
};
|
|
224
|
+
|
|
216
225
|
// Group consecutive same-role siblings. [a,a,b,a,a,a] → [[a,a],[b],[a,a,a]]
|
|
217
226
|
const groupByConsecutiveRole = (nodes: AriaNode[]): AriaNode[][] =>
|
|
218
227
|
nodes.reduce<AriaNode[][]>((groups, node) => {
|
|
@@ -239,15 +248,15 @@ const collapseGroup = (group: AriaNode[], depth: number): RenderEntry[] => {
|
|
|
239
248
|
const collapseSiblingGroups = (nodes: AriaNode[], depth: number): RenderEntry[] => groupByConsecutiveRole(nodes).flatMap((group) => collapseGroup(group, depth));
|
|
240
249
|
|
|
241
250
|
// Tree → indented YAML text.
|
|
242
|
-
const renderTree = (nodes: AriaNode[], depth = 0): string =>
|
|
251
|
+
const renderTree = (nodes: AriaNode[], depth = 0, offload?: ValueOffload): string =>
|
|
243
252
|
collapseSiblingGroups(nodes, depth)
|
|
244
253
|
.map((entry) => {
|
|
245
254
|
if ('placeholder' in entry) return entry.placeholder;
|
|
246
255
|
const { node } = entry;
|
|
247
256
|
const indent = ' '.repeat(depth);
|
|
248
|
-
const head = `${indent}- ${formatNode(node)}`;
|
|
257
|
+
const head = `${indent}- ${formatNode(node, offload)}`;
|
|
249
258
|
if (node.children.length === 0) return head;
|
|
250
|
-
return `${head}:\n${renderTree(node.children, depth + 1)}`;
|
|
259
|
+
return `${head}:\n${renderTree(node.children, depth + 1, offload)}`;
|
|
251
260
|
})
|
|
252
261
|
.join('\n');
|
|
253
262
|
|
|
@@ -382,6 +391,40 @@ const detectToggles = (prev: FlatEntry[], curr: FlatEntry[]): { toggled: string[
|
|
|
382
391
|
return { toggled, togglePaths };
|
|
383
392
|
};
|
|
384
393
|
|
|
394
|
+
const detectValueChanges = (prev: FlatEntry[], curr: FlatEntry[]): { typed: string[]; typedPaths: Set<string> } => {
|
|
395
|
+
const typed: string[] = [];
|
|
396
|
+
const typedPaths = new Set<string>();
|
|
397
|
+
const currByPath = new Map(curr.map((e) => [e.path, e]));
|
|
398
|
+
|
|
399
|
+
for (const before of prev) {
|
|
400
|
+
const after = currByPath.get(before.path);
|
|
401
|
+
if (!after) continue;
|
|
402
|
+
if (before.entry.role !== after.entry.role) continue;
|
|
403
|
+
if (before.entry.name !== after.entry.name) continue;
|
|
404
|
+
|
|
405
|
+
const was = valueWord(before.entry.value);
|
|
406
|
+
const now = valueWord(after.entry.value);
|
|
407
|
+
if (was === now) continue;
|
|
408
|
+
|
|
409
|
+
typedPaths.add(before.path);
|
|
410
|
+
let label = String(after.entry.role);
|
|
411
|
+
const name = after.entry.name;
|
|
412
|
+
if (typeof name === 'string' && name.trim()) label += ` "${name.trim()}"`;
|
|
413
|
+
typed.push(`${label}: ${was} -> ${now}`);
|
|
414
|
+
}
|
|
415
|
+
return { typed, typedPaths };
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const VALUE_EXCERPT = 60;
|
|
419
|
+
|
|
420
|
+
const valueWord = (value: unknown): string => {
|
|
421
|
+
if (value === undefined || value === null) return 'empty';
|
|
422
|
+
const text = String(value).trim();
|
|
423
|
+
if (!text) return 'empty';
|
|
424
|
+
if (text.length <= VALUE_EXCERPT) return JSON.stringify(text);
|
|
425
|
+
return `${JSON.stringify(text.slice(0, VALUE_EXCERPT))}… (${text.length} chars)`;
|
|
426
|
+
};
|
|
427
|
+
|
|
385
428
|
const TOP_DIFF_ITEMS = 10;
|
|
386
429
|
|
|
387
430
|
const formatDiffSection = (label: string, items: string[]): string[] => {
|
|
@@ -406,9 +449,13 @@ const formatDiffSection = (label: string, items: string[]): string[] => {
|
|
|
406
449
|
return lines;
|
|
407
450
|
};
|
|
408
451
|
|
|
409
|
-
const formatDiff = (added: string[], removed: string[], toggled: string[]): string | null => {
|
|
410
|
-
if (added.length === 0 && removed.length === 0 && toggled.length === 0) return null;
|
|
452
|
+
const formatDiff = (added: string[], removed: string[], toggled: string[], typed: string[] = []): string | null => {
|
|
453
|
+
if (added.length === 0 && removed.length === 0 && toggled.length === 0 && typed.length === 0) return null;
|
|
411
454
|
const sections = ['ariaDiff:'];
|
|
455
|
+
if (typed.length > 0) {
|
|
456
|
+
sections.push(' typed:');
|
|
457
|
+
for (const line of typed) sections.push(` - ${line}`);
|
|
458
|
+
}
|
|
412
459
|
if (toggled.length > 0) {
|
|
413
460
|
sections.push(' toggled:');
|
|
414
461
|
for (const line of toggled) sections.push(` - ${line}`);
|
|
@@ -474,13 +521,15 @@ const findDialogOrModal = (nodes: AriaNode[]): FocusAreaResult | null => {
|
|
|
474
521
|
// Public API — pipelines composed visibly, top-to-bottom
|
|
475
522
|
// ─────────────────────────────────────────────────────────────────
|
|
476
523
|
|
|
477
|
-
export
|
|
524
|
+
export type ValueOffload = (value: string) => string | undefined;
|
|
525
|
+
|
|
526
|
+
export const compactAriaSnapshot = (snapshot: string | null, keepNamed = false, offload?: ValueOffload): string => {
|
|
478
527
|
if (!snapshot) return '';
|
|
479
528
|
let tree = parseSnapshot(snapshot);
|
|
480
529
|
tree = unwrapIgnored(tree);
|
|
481
530
|
tree = nameIconButtons(tree);
|
|
482
531
|
tree = dropEmpty(tree, { keepNamed });
|
|
483
|
-
return renderTree(tree);
|
|
532
|
+
return renderTree(tree, 0, offload);
|
|
484
533
|
};
|
|
485
534
|
|
|
486
535
|
export const diffAriaSnapshots = (previous: string | null, current: string | null): AriaDiff => {
|
|
@@ -494,15 +543,17 @@ export const diffAriaSnapshots = (previous: string | null, current: string | nul
|
|
|
494
543
|
const prevAll = flat(previous);
|
|
495
544
|
const currAll = flat(current);
|
|
496
545
|
const { toggled, togglePaths } = detectToggles(prevAll, currAll);
|
|
497
|
-
const
|
|
498
|
-
const
|
|
546
|
+
const { typed, typedPaths } = detectValueChanges(prevAll, currAll);
|
|
547
|
+
const skip = (entry: FlatEntry) => togglePaths.has(entry.path) || typedPaths.has(entry.path);
|
|
548
|
+
const prev = prevAll.filter((e) => !skip(e));
|
|
549
|
+
const curr = currAll.filter((e) => !skip(e));
|
|
499
550
|
const prevTotals = countBy(prev.map((e) => e.summary));
|
|
500
551
|
const currTotals = countBy(curr.map((e) => e.summary));
|
|
501
552
|
const byCount = diffByCount(prevTotals, currTotals);
|
|
502
553
|
const renames = detectRenames(prev, curr, prevTotals, currTotals);
|
|
503
554
|
const added = [...byCount.added, ...renames.added];
|
|
504
555
|
const removed = [...byCount.removed, ...renames.removed];
|
|
505
|
-
return { text: formatDiff(added, removed, toggled), count: added.length + removed.length + toggled.length };
|
|
556
|
+
return { text: formatDiff(added, removed, toggled, typed), count: added.length + removed.length + toggled.length + typed.length };
|
|
506
557
|
};
|
|
507
558
|
|
|
508
559
|
export const detectFocusArea = (snapshot: string | null): FocusAreaResult => {
|
|
@@ -531,37 +582,6 @@ export const collectInteractiveNodes = (snapshot: string | null): Array<Record<s
|
|
|
531
582
|
// Standalone helpers (regex on raw strings — not part of the pipeline)
|
|
532
583
|
// ─────────────────────────────────────────────────────────────────
|
|
533
584
|
|
|
534
|
-
export interface FocusedElementInfo {
|
|
535
|
-
role: string;
|
|
536
|
-
name: string;
|
|
537
|
-
value?: string;
|
|
538
|
-
attributes?: string[];
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
export function extractFocusedElement(ariaSnapshot: string | null): FocusedElementInfo | null {
|
|
542
|
-
if (!ariaSnapshot) return null;
|
|
543
|
-
|
|
544
|
-
const focusedMatch = ariaSnapshot.match(/-\s*(\w+)\s+"([^"]*)"([^:\n]*)\[focused\](?::\s*(.*))?/);
|
|
545
|
-
if (!focusedMatch) return null;
|
|
546
|
-
|
|
547
|
-
const [, role, name, attributesStr, value] = focusedMatch;
|
|
548
|
-
|
|
549
|
-
const attributes: string[] = [];
|
|
550
|
-
if (attributesStr) {
|
|
551
|
-
const attrMatches = attributesStr.matchAll(/\[([^\]]+)\]/g);
|
|
552
|
-
for (const match of attrMatches) {
|
|
553
|
-
if (match[1] !== 'focused') {
|
|
554
|
-
attributes.push(match[1]);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const result: FocusedElementInfo = { role, name };
|
|
560
|
-
if (value) result.value = value.trim();
|
|
561
|
-
if (attributes.length > 0) result.attributes = attributes;
|
|
562
|
-
return result;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
585
|
export function parseAriaLocator(ariaStr: string): { role: string; text: string } | null {
|
|
566
586
|
const trimmed = ariaStr.trim();
|
|
567
587
|
if (trimmed === '-' || trimmed === '' || trimmed === '"-"') return null;
|
package/src/utils/html.ts
CHANGED
|
@@ -1518,6 +1518,7 @@ function cleanElement(element: parse5TreeAdapter.Element): void {
|
|
|
1518
1518
|
attr.value = attr.value
|
|
1519
1519
|
.split(/\s+/)
|
|
1520
1520
|
.filter((className) => !/\d/.test(className))
|
|
1521
|
+
.filter((className) => !className.includes(':'))
|
|
1521
1522
|
.filter((className) => !TAILWIND_CLASS_PATTERNS.some((pattern) => pattern.test(className)))
|
|
1522
1523
|
.join(' ');
|
|
1523
1524
|
|
package/src/utils/logger.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { Observability } from '../observability.ts';
|
|
|
11
11
|
import { RecentStepFilter } from './log-filters.ts';
|
|
12
12
|
import { parseMarkdownToTerminal } from './markdown-terminal.ts';
|
|
13
13
|
|
|
14
|
-
export type LogType = 'info' | 'success' | 'error' | 'warning' | 'debug' | 'substep' | 'operation' | 'step' | 'multiline' | 'details' | 'html' | 'input';
|
|
14
|
+
export type LogType = 'info' | 'success' | 'error' | 'warning' | 'debug' | 'substep' | 'operation' | 'step' | 'multiline' | 'details' | 'html' | 'input' | 'data';
|
|
15
15
|
|
|
16
16
|
export interface TaggedLogEntry {
|
|
17
17
|
type: LogType;
|
|
@@ -24,7 +24,7 @@ export interface TaggedLogEntry {
|
|
|
24
24
|
|
|
25
25
|
type LogEntry = TaggedLogEntry;
|
|
26
26
|
|
|
27
|
-
interface LogDestination {
|
|
27
|
+
export interface LogDestination {
|
|
28
28
|
isEnabled(): boolean;
|
|
29
29
|
write(entry: TaggedLogEntry): void;
|
|
30
30
|
}
|
|
@@ -79,9 +79,11 @@ const debugFilter = new DebugFilter();
|
|
|
79
79
|
class ConsoleDestination implements LogDestination {
|
|
80
80
|
private verboseMode = false;
|
|
81
81
|
private forceEnabled = false;
|
|
82
|
+
private quiet = false;
|
|
82
83
|
private recentSteps = new RecentStepFilter();
|
|
83
84
|
|
|
84
85
|
isEnabled(): boolean {
|
|
86
|
+
if (this.quiet) return false;
|
|
85
87
|
return this.forceEnabled || !process.env.INK_RUNNING;
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -89,6 +91,10 @@ class ConsoleDestination implements LogDestination {
|
|
|
89
91
|
this.forceEnabled = enabled;
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
setQuiet(enabled: boolean): void {
|
|
95
|
+
this.quiet = enabled;
|
|
96
|
+
}
|
|
97
|
+
|
|
92
98
|
setVerboseMode(enabled: boolean): void {
|
|
93
99
|
this.verboseMode = enabled;
|
|
94
100
|
}
|
|
@@ -136,6 +142,8 @@ class DebugDestination implements LogDestination {
|
|
|
136
142
|
|
|
137
143
|
setVerboseMode(enabled: boolean): void {
|
|
138
144
|
this.verboseMode = enabled;
|
|
145
|
+
// the debug package reads DEBUG when it loads, so a namespace turned on later needs enabling by hand
|
|
146
|
+
if (enabled) debug.enable(process.env.DEBUG || 'explorbot:*');
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
write(namespace: string, ...args: any[]): void {
|
|
@@ -332,6 +340,7 @@ class Logger {
|
|
|
332
340
|
private span = new SpanDestination();
|
|
333
341
|
public react = new ReactDestination();
|
|
334
342
|
public captain = new CaptainDestination();
|
|
343
|
+
private extra: LogDestination[] = [];
|
|
335
344
|
private truncateTags: string[] = ['page_html'];
|
|
336
345
|
|
|
337
346
|
private constructor() {}
|
|
@@ -360,10 +369,19 @@ class Logger {
|
|
|
360
369
|
this.console.forceEnable(enabled);
|
|
361
370
|
}
|
|
362
371
|
|
|
372
|
+
setQuietMode(enabled: boolean): void {
|
|
373
|
+
this.console.setQuiet(enabled);
|
|
374
|
+
}
|
|
375
|
+
|
|
363
376
|
isVerboseMode(): boolean {
|
|
364
377
|
return this.debugDestination.isEnabled();
|
|
365
378
|
}
|
|
366
379
|
|
|
380
|
+
addDestination(destination: LogDestination): void {
|
|
381
|
+
if (this.extra.includes(destination)) return;
|
|
382
|
+
this.extra.push(destination);
|
|
383
|
+
}
|
|
384
|
+
|
|
367
385
|
registerLogPane(addLog: (entry: LogEntry) => void): void {
|
|
368
386
|
this.react.registerLogPane(addLog);
|
|
369
387
|
}
|
|
@@ -440,6 +458,14 @@ class Logger {
|
|
|
440
458
|
return;
|
|
441
459
|
}
|
|
442
460
|
|
|
461
|
+
if (type === 'data') {
|
|
462
|
+
const entry: TaggedLogEntry = { type, content: String(args[0]), timestamp: new Date(), originalArgs: args };
|
|
463
|
+
for (const destination of this.extra) {
|
|
464
|
+
if (destination.isEnabled()) destination.write(entry);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
443
469
|
const options = this.extractLogOptions(type, args);
|
|
444
470
|
let content = this.processArgs(args);
|
|
445
471
|
if (type === 'step' && args[0]?.toCode) {
|
|
@@ -456,6 +482,9 @@ class Logger {
|
|
|
456
482
|
if (this.file.isEnabled()) this.file.write(entry);
|
|
457
483
|
if (this.span.isEnabled()) this.span.write(entry);
|
|
458
484
|
if (this.captain.isEnabled()) this.captain.write(entry);
|
|
485
|
+
for (const destination of this.extra) {
|
|
486
|
+
if (destination.isEnabled()) destination.write(entry);
|
|
487
|
+
}
|
|
459
488
|
if (process.env.INK_RUNNING) {
|
|
460
489
|
this.react.write(entry);
|
|
461
490
|
} else if (this.console.isEnabled()) {
|
|
@@ -537,10 +566,12 @@ export const startLogCapture = () => logger.captain.startCapture();
|
|
|
537
566
|
export const stopLogCapture = () => logger.captain.stopCapture();
|
|
538
567
|
export const setVerboseMode = (enabled: boolean) => logger.setVerboseMode(enabled);
|
|
539
568
|
export const setPreserveConsoleLogs = (enabled: boolean) => logger.setPreserveConsoleLogs(enabled);
|
|
569
|
+
export const setQuietMode = (enabled: boolean) => logger.setQuietMode(enabled);
|
|
540
570
|
export const isVerboseMode = () => logger.isVerboseMode();
|
|
541
571
|
export const setDebugMode = (enabled: boolean) => logger.setDebugMode(enabled);
|
|
542
572
|
export const isDebugMode = () => logger.isDebugMode();
|
|
543
573
|
|
|
574
|
+
export const addDestination = (destination: LogDestination) => logger.addDestination(destination);
|
|
544
575
|
export const registerLogPane = (addLog: (entry: LogEntry) => void) => logger.registerLogPane(addLog);
|
|
545
576
|
export const unregisterLogPane = (addLog: (entry: LogEntry) => void) => logger.unregisterLogPane(addLog);
|
|
546
577
|
export const addTruncateTag = (tagName: string) => logger.addTruncateTag(tagName);
|
|
@@ -4,10 +4,33 @@ export async function waitForPageReadiness(page: any, options: PageReadinessOpti
|
|
|
4
4
|
const timeout = options.timeout ?? 6000;
|
|
5
5
|
await page.waitForLoadState?.('domcontentloaded', { timeout })?.catch(() => {});
|
|
6
6
|
|
|
7
|
-
await Promise.race([waitForNetworkIdle(page, timeout), waitForVisibleSpinnersHidden(page, options.spinnerSelectors || [], timeout), sleep(timeout)]).catch(() => {});
|
|
7
|
+
await Promise.race([waitForNetworkIdle(page, timeout), waitForDomQuiet(page, timeout), waitForVisibleSpinnersHidden(page, options.spinnerSelectors || [], timeout), sleep(timeout)]).catch(() => {});
|
|
8
8
|
await waitForPageBodyContent(page, timeout);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const DOM_QUIET_MS = 350;
|
|
12
|
+
|
|
13
|
+
function waitForDomQuiet(page: any, timeout: number): Promise<void> {
|
|
14
|
+
if (!page?.waitForFunction) return new Promise(() => {});
|
|
15
|
+
|
|
16
|
+
return page
|
|
17
|
+
.waitForFunction(
|
|
18
|
+
(quiet: number) => {
|
|
19
|
+
const store = window as any;
|
|
20
|
+
if (!store.__explorbotDomQuiet) {
|
|
21
|
+
store.__explorbotDomQuiet = { last: Date.now() };
|
|
22
|
+
new MutationObserver(() => {
|
|
23
|
+
store.__explorbotDomQuiet.last = Date.now();
|
|
24
|
+
}).observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
25
|
+
}
|
|
26
|
+
return Date.now() - store.__explorbotDomQuiet.last >= quiet;
|
|
27
|
+
},
|
|
28
|
+
DOM_QUIET_MS,
|
|
29
|
+
{ timeout, polling: 100 }
|
|
30
|
+
)
|
|
31
|
+
.catch(() => {});
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
function waitForNetworkIdle(page: any, timeout: number): Promise<void> {
|
|
12
35
|
if (!page?.waitForLoadState) return Promise.resolve();
|
|
13
36
|
return page.waitForLoadState('networkidle', { timeout }).catch(() => {});
|
package/src/utils/url-matcher.ts
CHANGED
|
@@ -100,6 +100,9 @@ export function matchesNavigationUrl(expected: string, current: string): boolean
|
|
|
100
100
|
if (!expectedPath.includes('#')) {
|
|
101
101
|
currentPath = currentPath.split('#')[0];
|
|
102
102
|
}
|
|
103
|
+
if (!expectedPath.includes('?')) {
|
|
104
|
+
currentPath = currentPath.split('?')[0];
|
|
105
|
+
}
|
|
103
106
|
const normalize = (value: string) => value.replace(/^\/+|\/+$/g, '').toLowerCase();
|
|
104
107
|
return normalize(expectedPath) === normalize(currentPath);
|
|
105
108
|
}
|
package/src/utils/web-element.ts
CHANGED
|
@@ -125,6 +125,15 @@ export class WebElement {
|
|
|
125
125
|
return WebElement.fromPlaywrightLocator(page.locator(`[${EXPLORBOT_ATTRS.eidx}="${eidx}"]`));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
static isAriaRef(ref: string): boolean {
|
|
129
|
+
return /^(f\d+)?e\d+$/i.test(ref);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static async fromAriaRef(page: any, ref: string): Promise<WebElement | null> {
|
|
133
|
+
if (!WebElement.isAriaRef(ref)) return null;
|
|
134
|
+
return WebElement.fromPlaywrightLocator(page.locator(`aria-ref=${ref}`));
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
static async fromEidxList(page: any, eidxList: string[]): Promise<WebElement[]> {
|
|
129
138
|
const validEidxList = eidxList.filter((eidx) => /^e\d+$/i.test(eidx));
|
|
130
139
|
if (validEidxList.length === 0) return [];
|
package/src/utils/web-sandbox.ts
CHANGED
|
@@ -28,13 +28,12 @@ export function playwrightSandbox(page: any, code: string): Promise<any> {
|
|
|
28
28
|
return run(page);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function codeceptJSSandbox(actor: any, codeOrFn: string | ((...args: any[]) => void)):
|
|
31
|
+
export function codeceptJSSandbox(actor: any, codeOrFn: string | ((...args: any[]) => void)): any {
|
|
32
32
|
if (typeof codeOrFn === 'function') {
|
|
33
|
-
codeOrFn(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
34
|
-
return;
|
|
33
|
+
return codeOrFn(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
35
34
|
}
|
|
36
35
|
const run = createSandbox(CODECEPT_ARG_NAMES, codeOrFn);
|
|
37
|
-
run(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
36
|
+
return run(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
function createSandbox(argNames: string[], body: string): (...args: any[]) => any {
|