explorbot 0.1.28 → 0.1.30
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 +83 -245
- package/bin/explorbot-cli.ts +1 -0
- package/boat/doc-collector/src/ai/documentarian.ts +37 -13
- package/boat/doc-collector/src/ai/tools.ts +60 -20
- package/boat/doc-collector/src/cli.ts +3 -0
- package/boat/doc-collector/src/config.ts +7 -0
- package/boat/doc-collector/src/docbot.ts +23 -5
- package/boat/doc-collector/src/docs-renderer.ts +14 -1
- package/boat/doc-collector/src/screenshots.ts +126 -0
- package/dist/bin/explorbot-cli.js +1 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +15 -11
- package/dist/boat/doc-collector/src/ai/tools.js +53 -20
- package/dist/boat/doc-collector/src/cli.js +3 -0
- package/dist/boat/doc-collector/src/config.js +3 -0
- package/dist/boat/doc-collector/src/docbot.js +19 -4
- package/dist/boat/doc-collector/src/docs-renderer.js +12 -1
- package/dist/boat/doc-collector/src/screenshots.js +90 -0
- package/dist/package.json +8 -6
- package/dist/rules/navigator/verification-actions.md +2 -0
- package/dist/src/action.js +26 -23
- package/dist/src/ai/fisherman.js +14 -3
- package/dist/src/ai/historian/codeceptjs.js +3 -2
- package/dist/src/ai/historian/experience.js +48 -6
- package/dist/src/ai/historian/playwright.js +2 -1
- package/dist/src/ai/historian/utils.js +1 -19
- package/dist/src/ai/historian.js +1 -1
- package/dist/src/ai/pilot.js +19 -4
- package/dist/src/ai/planner.js +16 -5
- package/dist/src/ai/provider.js +53 -18
- package/dist/src/ai/quartermaster.js +2 -2
- package/dist/src/ai/researcher.js +7 -1
- package/dist/src/ai/rules.js +44 -0
- package/dist/src/ai/tester.js +73 -7
- package/dist/src/ai/tools.js +66 -1
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.js +14 -3
- package/dist/src/explorer.js +30 -27
- package/dist/src/stats.js +16 -0
- package/dist/src/utils/aria.js +66 -6
- package/dist/src/utils/browser-errors.js +5 -0
- package/dist/src/utils/page-readiness.js +48 -0
- package/dist/src/utils/step-analyzer.js +68 -0
- package/package.json +8 -6
- package/rules/navigator/verification-actions.md +2 -0
- package/src/action.ts +24 -26
- package/src/ai/fisherman.ts +14 -3
- package/src/ai/historian/codeceptjs.ts +3 -2
- package/src/ai/historian/experience.ts +51 -6
- package/src/ai/historian/playwright.ts +2 -1
- package/src/ai/historian/utils.ts +1 -21
- package/src/ai/historian.ts +1 -1
- package/src/ai/pilot.ts +19 -4
- package/src/ai/planner.ts +16 -5
- package/src/ai/provider.ts +51 -19
- package/src/ai/quartermaster.ts +2 -2
- package/src/ai/researcher.ts +8 -1
- package/src/ai/rules.ts +46 -0
- package/src/ai/tester.ts +77 -7
- package/src/ai/tools.ts +79 -1
- package/src/config.ts +2 -0
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +13 -3
- package/src/explorer.ts +28 -27
- package/src/stats.ts +18 -0
- package/src/utils/aria.ts +63 -6
- package/src/utils/browser-errors.ts +6 -0
- package/src/utils/page-readiness.ts +59 -0
- package/src/utils/step-analyzer.ts +73 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export async function waitForPageReadiness(page: any, options: PageReadinessOptions = {}): Promise<void> {
|
|
2
|
+
if (!page) return;
|
|
3
|
+
|
|
4
|
+
const timeout = options.timeout ?? 6000;
|
|
5
|
+
await page.waitForLoadState?.('domcontentloaded', { timeout })?.catch(() => {});
|
|
6
|
+
|
|
7
|
+
await Promise.race([waitForNetworkIdle(page, timeout), waitForVisibleSpinnersHidden(page, options.spinnerSelectors || [], timeout), sleep(timeout)]).catch(() => {});
|
|
8
|
+
await waitForPageBodyContent(page, timeout);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function waitForNetworkIdle(page: any, timeout: number): Promise<void> {
|
|
12
|
+
if (!page?.waitForLoadState) return Promise.resolve();
|
|
13
|
+
return page.waitForLoadState('networkidle', { timeout }).catch(() => {});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function waitForVisibleSpinnersHidden(page: any, selectors: string[], timeout: number): Promise<void> {
|
|
17
|
+
if (!selectors.length) return new Promise(() => {});
|
|
18
|
+
if (!page?.locator) return new Promise(() => {});
|
|
19
|
+
|
|
20
|
+
const visibleSpinners = [];
|
|
21
|
+
for (const selector of selectors) {
|
|
22
|
+
const locator = page.locator(selector);
|
|
23
|
+
const isVisible = await locator
|
|
24
|
+
.first()
|
|
25
|
+
.isVisible({ timeout: 100 })
|
|
26
|
+
.catch(() => false);
|
|
27
|
+
if (!isVisible) continue;
|
|
28
|
+
visibleSpinners.push(locator);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (visibleSpinners.length === 0) return new Promise(() => {});
|
|
32
|
+
|
|
33
|
+
return Promise.all(visibleSpinners.map((locator) => locator.waitFor({ state: 'hidden', timeout }).catch(() => {}))).then(() => {});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function waitForPageBodyContent(page: any, timeout: number): Promise<void> {
|
|
37
|
+
if (!page?.waitForFunction) return Promise.resolve();
|
|
38
|
+
|
|
39
|
+
return page
|
|
40
|
+
.waitForFunction(
|
|
41
|
+
() => {
|
|
42
|
+
const body = document.body;
|
|
43
|
+
if (!body) return false;
|
|
44
|
+
return body.children.length > 0 || body.textContent?.trim().length > 0;
|
|
45
|
+
},
|
|
46
|
+
undefined,
|
|
47
|
+
{ timeout }
|
|
48
|
+
)
|
|
49
|
+
.catch(() => {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sleep(ms: number): Promise<void> {
|
|
53
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface PageReadinessOptions {
|
|
57
|
+
timeout?: number;
|
|
58
|
+
spinnerSelectors?: string[];
|
|
59
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { SessionStep } from '../experience-tracker.ts';
|
|
2
|
+
import type { StepData } from '../test-plan.ts';
|
|
3
|
+
import { isDynamicId } from './xpath.ts';
|
|
4
|
+
|
|
5
|
+
export const CODECEPT_TOOLS = ['click', 'hover', 'pressKey', 'form'] as const;
|
|
6
|
+
export type CodeceptToolName = (typeof CODECEPT_TOOLS)[number];
|
|
7
|
+
|
|
8
|
+
const CODECEPT_FORM_COMMANDS: readonly string[] = ['I.fillField', 'I.type', 'I.selectOption', 'I.attachFile', 'I.checkOption', 'I.uncheckOption'];
|
|
9
|
+
|
|
10
|
+
export function isCodeceptToolName(toolName: string): toolName is CodeceptToolName {
|
|
11
|
+
return CODECEPT_TOOLS.includes(toolName as CodeceptToolName);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCodeceptToolName(commandName: string): CodeceptToolName | null {
|
|
15
|
+
const toolName = CODECEPT_TOOLS.find((name) => commandName === `I.${name}`);
|
|
16
|
+
if (toolName) return toolName;
|
|
17
|
+
if (CODECEPT_FORM_COMMANDS.includes(commandName)) return 'form';
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getCodeceptToolNameFromCode(code: string): CodeceptToolName | null {
|
|
22
|
+
const parenIndex = code.trim().indexOf('(');
|
|
23
|
+
if (parenIndex < 1) return null;
|
|
24
|
+
return getCodeceptToolName(code.trim().slice(0, parenIndex));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function stripComments(code: string): string {
|
|
28
|
+
return code
|
|
29
|
+
.split('\n')
|
|
30
|
+
.filter((line) => {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
return trimmed && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*');
|
|
33
|
+
})
|
|
34
|
+
.join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isNonReusableCode(code: string): boolean {
|
|
38
|
+
if (/\bI\.clickXY\s*\(/.test(code)) return true;
|
|
39
|
+
|
|
40
|
+
for (const m of code.matchAll(/#([A-Za-z_][\w-]*)/g)) {
|
|
41
|
+
if (isDynamicId(m[1])) return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function toReusableSessionStep(step: StepData): SessionStep | null {
|
|
48
|
+
if (step.status !== 'passed') return null;
|
|
49
|
+
const code = stripComments(step.text);
|
|
50
|
+
if (!code || isNonReusableCode(code)) return null;
|
|
51
|
+
const toolName = getCodeceptToolNameFromCode(code);
|
|
52
|
+
if (!toolName) return null;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
message: step.text,
|
|
56
|
+
status: 'passed',
|
|
57
|
+
tool: toolName,
|
|
58
|
+
code,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function mergeUniqueStepsByCode(primary: SessionStep[], secondary: SessionStep[]): SessionStep[] {
|
|
63
|
+
const merged: SessionStep[] = [];
|
|
64
|
+
const seen = new Set<string>();
|
|
65
|
+
for (const step of [...primary, ...secondary]) {
|
|
66
|
+
const identity = stripComments(step.code || '').trim();
|
|
67
|
+
if (!identity) continue;
|
|
68
|
+
if (seen.has(identity)) continue;
|
|
69
|
+
seen.add(identity);
|
|
70
|
+
merged.push(step);
|
|
71
|
+
}
|
|
72
|
+
return merged;
|
|
73
|
+
}
|