explorbot 0.1.10 → 0.1.11
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 +27 -1
- package/bin/explorbot-cli.ts +27 -18
- package/dist/bin/explorbot-cli.js +26 -18
- package/dist/package.json +2 -2
- package/dist/rules/navigator/output.md +9 -0
- package/dist/rules/navigator/verification-actions.md +2 -0
- package/dist/src/action-result.js +23 -1
- package/dist/src/action.js +46 -38
- package/dist/src/ai/bosun.js +11 -1
- package/dist/src/ai/conversation.js +39 -0
- package/dist/src/ai/historian/codeceptjs.js +109 -0
- package/dist/src/ai/historian/experience.js +320 -0
- package/dist/src/ai/historian/mixin.js +2 -0
- package/dist/src/ai/historian/playwright.js +145 -0
- package/dist/src/ai/historian/utils.js +18 -0
- package/dist/src/ai/historian.js +19 -405
- package/dist/src/ai/navigator.js +82 -29
- package/dist/src/ai/pilot.js +232 -13
- package/dist/src/ai/planner.js +29 -9
- package/dist/src/ai/provider.js +54 -17
- package/dist/src/ai/researcher.js +41 -32
- package/dist/src/ai/rules.js +26 -14
- package/dist/src/ai/tester.js +90 -26
- package/dist/src/ai/tools.js +13 -7
- package/dist/src/browser-server.js +16 -3
- package/dist/src/commands/add-rule-command.js +11 -8
- package/dist/src/commands/clean-command.js +2 -1
- package/dist/src/commands/explore-command.js +27 -15
- package/dist/src/commands/init-command.js +9 -8
- package/dist/src/commands/plan-command.js +32 -0
- package/dist/src/commands/plan-save-command.js +19 -7
- package/dist/src/commands/rerun-command.js +4 -0
- package/dist/src/components/App.js +15 -5
- package/dist/src/execution-controller.js +13 -2
- package/dist/src/experience-tracker.js +20 -64
- package/dist/src/explorbot.js +5 -8
- package/dist/src/explorer.js +9 -2
- package/dist/src/observability.js +50 -99
- package/dist/src/playwright-recorder.js +309 -0
- package/dist/src/test-plan.js +12 -0
- package/dist/src/utils/aria.js +37 -1
- package/dist/src/utils/error-page.js +20 -7
- package/dist/src/utils/next-steps.js +37 -0
- package/package.json +2 -2
- package/rules/navigator/output.md +9 -0
- package/rules/navigator/verification-actions.md +2 -0
- package/src/action-result.ts +26 -1
- package/src/action.ts +44 -37
- package/src/ai/bosun.ts +11 -1
- package/src/ai/conversation.ts +37 -0
- package/src/ai/historian/codeceptjs.ts +130 -0
- package/src/ai/historian/experience.ts +383 -0
- package/src/ai/historian/mixin.ts +4 -0
- package/src/ai/historian/playwright.ts +169 -0
- package/src/ai/historian/utils.ts +23 -0
- package/src/ai/historian.ts +35 -473
- package/src/ai/navigator.ts +82 -29
- package/src/ai/pilot.ts +237 -14
- package/src/ai/planner.ts +29 -9
- package/src/ai/provider.ts +51 -17
- package/src/ai/researcher.ts +45 -33
- package/src/ai/rules.ts +27 -14
- package/src/ai/tester.ts +94 -26
- package/src/ai/tools.ts +47 -25
- package/src/browser-server.ts +17 -3
- package/src/commands/add-rule-command.ts +11 -7
- package/src/commands/clean-command.ts +2 -1
- package/src/commands/explore-command.ts +29 -15
- package/src/commands/init-command.ts +9 -8
- package/src/commands/plan-command.ts +35 -0
- package/src/commands/plan-save-command.ts +18 -7
- package/src/commands/rerun-command.ts +5 -0
- package/src/components/App.tsx +16 -5
- package/src/config.ts +6 -1
- package/src/execution-controller.ts +14 -3
- package/src/experience-tracker.ts +21 -72
- package/src/explorbot.ts +5 -8
- package/src/explorer.ts +11 -2
- package/src/observability.ts +50 -109
- package/src/playwright-recorder.ts +305 -0
- package/src/test-plan.ts +12 -0
- package/src/utils/aria.ts +38 -1
- package/src/utils/error-page.ts +22 -7
- package/src/utils/next-steps.ts +51 -0
package/src/action.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { ConfigParser, outputPath } from './config.js';
|
|
|
16
16
|
import type { ExplorbotConfig } from './config.js';
|
|
17
17
|
import type { UserResolveFunction } from './explorbot.ts';
|
|
18
18
|
import { Observability } from './observability.ts';
|
|
19
|
+
import type { PlaywrightRecorder } from './playwright-recorder.ts';
|
|
19
20
|
import type { StateManager } from './state-manager.js';
|
|
20
21
|
import { extractCodeBlocks } from './utils/code-extractor.js';
|
|
21
22
|
import { htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
@@ -36,12 +37,16 @@ class Action {
|
|
|
36
37
|
private expectation: string | null = null;
|
|
37
38
|
public lastError: Error | null = null;
|
|
38
39
|
public playwrightHelper: any;
|
|
40
|
+
public playwrightGroupId: string | null = null;
|
|
41
|
+
public assertionSteps: Array<{ name: string; args: any[] }> = [];
|
|
42
|
+
private recorder?: PlaywrightRecorder;
|
|
39
43
|
|
|
40
|
-
constructor(actor: CodeceptJS.I, stateManager: StateManager) {
|
|
44
|
+
constructor(actor: CodeceptJS.I, stateManager: StateManager, recorder?: PlaywrightRecorder) {
|
|
41
45
|
this.actor = actor;
|
|
42
46
|
this.stateManager = stateManager;
|
|
43
47
|
this.config = ConfigParser.getInstance().getConfig();
|
|
44
48
|
this.playwrightHelper = container.helpers('Playwright');
|
|
49
|
+
this.recorder = recorder;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
async caputrePageWithScreenshot(): Promise<ActionResult> {
|
|
@@ -71,7 +76,14 @@ class Action {
|
|
|
71
76
|
const timestamp = Date.now();
|
|
72
77
|
const page = this.playwrightHelper.page;
|
|
73
78
|
const frame = this.playwrightHelper.frame;
|
|
74
|
-
|
|
79
|
+
await page?.waitForLoadState('domcontentloaded', { timeout: 10000 })?.catch(() => {});
|
|
80
|
+
const grabAll = () => Promise.all([(this.actor as any).grabSource(), (this.actor as any).grabTitle(), this.captureBrowserLogs()]);
|
|
81
|
+
const [html, title, browserLogs] = await grabAll().catch(async (err: Error) => {
|
|
82
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
83
|
+
if (!/navigating and changing the content/i.test(msg)) throw err;
|
|
84
|
+
await page?.waitForLoadState('domcontentloaded', { timeout: 10000 })?.catch(() => {});
|
|
85
|
+
return grabAll();
|
|
86
|
+
});
|
|
75
87
|
const url = page?.url() || (await (this.actor as any).grabCurrentUrl?.());
|
|
76
88
|
|
|
77
89
|
let screenshotFile: string | undefined = undefined;
|
|
@@ -218,7 +230,10 @@ class Action {
|
|
|
218
230
|
let codeString = code.replace(/^\(I\) => /, '').trim();
|
|
219
231
|
|
|
220
232
|
const executedSteps: string[] = [];
|
|
221
|
-
|
|
233
|
+
const assertionSteps: Array<{ name: string; args: any[] }> = [];
|
|
234
|
+
const stepListener = attachStepLogger(executedSteps, assertionSteps);
|
|
235
|
+
const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
|
|
236
|
+
this.playwrightGroupId = groupId;
|
|
222
237
|
const activeSpan = Observability.getSpan();
|
|
223
238
|
const tracer = trace.getTracer('ai');
|
|
224
239
|
const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
|
|
@@ -253,6 +268,7 @@ class Action {
|
|
|
253
268
|
this.stateManager.updateState(pageState, codeString);
|
|
254
269
|
|
|
255
270
|
this.actionResult = pageState;
|
|
271
|
+
this.assertionSteps = assertionSteps;
|
|
256
272
|
} catch (err) {
|
|
257
273
|
debugLog('Action error', errorToString(err));
|
|
258
274
|
error = err as Error;
|
|
@@ -260,9 +276,11 @@ class Action {
|
|
|
260
276
|
await recorder.reset();
|
|
261
277
|
await recorder.start();
|
|
262
278
|
}
|
|
279
|
+
this.assertionSteps = [];
|
|
263
280
|
throw err;
|
|
264
281
|
} finally {
|
|
265
|
-
|
|
282
|
+
if (groupId) await this.recorder!.endAction();
|
|
283
|
+
detachStepLogger(stepListener);
|
|
266
284
|
if (stepSpan) {
|
|
267
285
|
stepSpan.end();
|
|
268
286
|
}
|
|
@@ -407,41 +425,30 @@ function sleep(ms: number) {
|
|
|
407
425
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
408
426
|
}
|
|
409
427
|
|
|
410
|
-
|
|
411
|
-
let stepLoggerTarget: string[] | null = null;
|
|
428
|
+
const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
|
|
412
429
|
|
|
413
|
-
|
|
414
|
-
if (!step?.toCode) {
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (step.name?.startsWith('grab')) return;
|
|
418
|
-
const stepCode = step.toCode();
|
|
419
|
-
if (stepLoggerTarget) {
|
|
420
|
-
stepLoggerTarget.push(stepCode);
|
|
421
|
-
}
|
|
422
|
-
if (error) {
|
|
423
|
-
tag('step').log(step, error);
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
tag('step').log(step);
|
|
427
|
-
};
|
|
430
|
+
type StepListener = (step: any, error?: any) => void;
|
|
428
431
|
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
432
|
+
const attachStepLogger = (target: string[], assertionsTarget?: Array<{ name: string; args: any[] }>): StepListener => {
|
|
433
|
+
const listener: StepListener = (step, error) => {
|
|
434
|
+
if (!step?.toCode) return;
|
|
435
|
+
if (step.name?.startsWith('grab')) return;
|
|
436
|
+
target.push(step.toCode());
|
|
437
|
+
if (assertionsTarget && ASSERTION_STEP_NAMES.has(step.name)) {
|
|
438
|
+
assertionsTarget.push({ name: step.name, args: step.args || [] });
|
|
439
|
+
}
|
|
440
|
+
if (error) {
|
|
441
|
+
tag('step').log(step, error);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
tag('step').log(step);
|
|
445
|
+
};
|
|
446
|
+
codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, listener);
|
|
447
|
+
codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, listener);
|
|
448
|
+
return listener;
|
|
437
449
|
};
|
|
438
450
|
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
stepLoggerRegistered = false;
|
|
445
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, stepLogger);
|
|
446
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, stepLogger);
|
|
451
|
+
const detachStepLogger = (listener: StepListener) => {
|
|
452
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
|
|
453
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
447
454
|
};
|
package/src/ai/bosun.ts
CHANGED
|
@@ -10,8 +10,10 @@ import { Observability } from '../observability.ts';
|
|
|
10
10
|
import { Plan, Task, Test, TestResult } from '../test-plan.ts';
|
|
11
11
|
import { diffAriaSnapshots } from '../utils/aria.ts';
|
|
12
12
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
13
|
+
import { getCliName } from '../utils/cli-name.ts';
|
|
13
14
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
14
15
|
import { loop, pause } from '../utils/loop.ts';
|
|
16
|
+
import { type NextStepSection, printNextSteps } from '../utils/next-steps.ts';
|
|
15
17
|
import type { Agent } from './agent.ts';
|
|
16
18
|
import type { Conversation } from './conversation.ts';
|
|
17
19
|
import type { Navigator } from './navigator.ts';
|
|
@@ -497,7 +499,15 @@ export class Bosun extends TaskAgent implements Agent {
|
|
|
497
499
|
const content = this.generateKnowledgeContent(state, successfulInteractions);
|
|
498
500
|
const result = knowledgeTracker.addKnowledge(knowledgePath, content);
|
|
499
501
|
|
|
500
|
-
|
|
502
|
+
const cli = getCliName();
|
|
503
|
+
const sections: NextStepSection[] = [
|
|
504
|
+
{
|
|
505
|
+
label: 'Knowledge',
|
|
506
|
+
path: result.filePath,
|
|
507
|
+
commands: [{ label: 'View matches', command: `${cli} knows ${knowledgePath}` }],
|
|
508
|
+
},
|
|
509
|
+
];
|
|
510
|
+
printNextSteps(sections);
|
|
501
511
|
}
|
|
502
512
|
|
|
503
513
|
private generateKnowledgeContent(state: any, interactions: InteractionResult[]): string {
|
package/src/ai/conversation.ts
CHANGED
|
@@ -11,6 +11,9 @@ export function toolExecutionLabel(input: Record<string, any> | undefined): stri
|
|
|
11
11
|
return input?.explanation || input?.assertion || input?.reason || input?.request || '';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const AUTO_COMPACT_ARIA_CHANGES_CUTOFF = 500;
|
|
15
|
+
const AUTO_COMPACT_TARGETED_HTML_CUTOFF = 500;
|
|
16
|
+
|
|
14
17
|
export class Conversation {
|
|
15
18
|
id: string;
|
|
16
19
|
messages: ModelMessage[];
|
|
@@ -132,6 +135,40 @@ export class Conversation {
|
|
|
132
135
|
this.autoTrimRules.set(tagName, maxLength);
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
compactToolResults(keepLastN: number): void {
|
|
139
|
+
const toolMessageIndexes: number[] = [];
|
|
140
|
+
for (let i = 0; i < this.messages.length; i++) {
|
|
141
|
+
if (this.messages[i].role === 'tool') toolMessageIndexes.push(i);
|
|
142
|
+
}
|
|
143
|
+
const compactUpTo = toolMessageIndexes.length - Math.max(0, keepLastN);
|
|
144
|
+
for (let k = 0; k < compactUpTo; k++) {
|
|
145
|
+
const message = this.messages[toolMessageIndexes[k]];
|
|
146
|
+
if (!Array.isArray(message.content)) continue;
|
|
147
|
+
for (const part of message.content) {
|
|
148
|
+
if (part.type !== 'tool-result') continue;
|
|
149
|
+
const rawOutput = part.output as Record<string, any> | undefined;
|
|
150
|
+
if (!rawOutput || rawOutput.type !== 'json' || !rawOutput.value || typeof rawOutput.value !== 'object') continue;
|
|
151
|
+
const value = rawOutput.value as Record<string, any>;
|
|
152
|
+
if (value.pageDiff && typeof value.pageDiff === 'object') {
|
|
153
|
+
const pageDiff = value.pageDiff as Record<string, any>;
|
|
154
|
+
if (Array.isArray(pageDiff.htmlParts)) {
|
|
155
|
+
pageDiff.htmlParts = undefined;
|
|
156
|
+
pageDiff.compacted = true;
|
|
157
|
+
}
|
|
158
|
+
if (typeof pageDiff.ariaChanges === 'string' && pageDiff.ariaChanges.length > AUTO_COMPACT_ARIA_CHANGES_CUTOFF) {
|
|
159
|
+
pageDiff.ariaChanges = `${pageDiff.ariaChanges.slice(0, AUTO_COMPACT_ARIA_CHANGES_CUTOFF)}...`;
|
|
160
|
+
}
|
|
161
|
+
if (typeof pageDiff.iframes === 'string') {
|
|
162
|
+
pageDiff.iframes = undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (typeof value.targetedHtml === 'string' && value.targetedHtml.length > AUTO_COMPACT_TARGETED_HTML_CUTOFF) {
|
|
166
|
+
value.targetedHtml = `${value.targetedHtml.slice(0, AUTO_COMPACT_TARGETED_HTML_CUTOFF)}...`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
135
172
|
hasTag(tagName: string, lastN?: number): boolean {
|
|
136
173
|
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
137
174
|
const regex = new RegExp(`<${escapedTag}>`, 'g');
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ActionResult } from '../../action-result.ts';
|
|
4
|
+
import { ConfigParser } from '../../config.ts';
|
|
5
|
+
import { KnowledgeTracker } from '../../knowledge-tracker.ts';
|
|
6
|
+
import type { Plan } from '../../test-plan.ts';
|
|
7
|
+
import { tag } from '../../utils/logger.ts';
|
|
8
|
+
import { relativeToCwd } from '../../utils/next-steps.ts';
|
|
9
|
+
import type { Conversation } from '../conversation.ts';
|
|
10
|
+
import { ASSERTION_TOOLS, CODECEPT_TOOLS } from '../tools.ts';
|
|
11
|
+
import type { Constructor } from './mixin.ts';
|
|
12
|
+
import { escapeString, getExecutionLabel, isNonReusableCode, stripComments } from './utils.ts';
|
|
13
|
+
|
|
14
|
+
export interface CodeceptJSMethods {
|
|
15
|
+
toCode(conversation: Conversation, scenario: string): string;
|
|
16
|
+
saveCodeceptPlanToFile(plan: Plan): string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function WithCodeceptJS<T extends Constructor>(Base: T) {
|
|
20
|
+
return class extends Base {
|
|
21
|
+
declare savedFiles: Set<string>;
|
|
22
|
+
|
|
23
|
+
toCode(conversation: Conversation, scenario: string): string {
|
|
24
|
+
const toolExecutions = conversation.getToolExecutions();
|
|
25
|
+
const TRACKABLE_TOOLS = [...CODECEPT_TOOLS, ...ASSERTION_TOOLS];
|
|
26
|
+
const successfulSteps = toolExecutions.filter((exec) => exec.wasSuccessful && TRACKABLE_TOOLS.includes(exec.toolName as any) && exec.output?.code);
|
|
27
|
+
|
|
28
|
+
if (successfulSteps.length === 0) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const lines: string[] = [];
|
|
33
|
+
lines.push(`Scenario('${escapeString(scenario)}', ({ I }) => {`);
|
|
34
|
+
|
|
35
|
+
for (const exec of successfulSteps) {
|
|
36
|
+
if (isNonReusableCode(exec.output.code)) continue;
|
|
37
|
+
const explanation = getExecutionLabel(exec);
|
|
38
|
+
if (explanation) {
|
|
39
|
+
lines.push('');
|
|
40
|
+
lines.push(` Section('${escapeString(explanation)}');`);
|
|
41
|
+
}
|
|
42
|
+
const code = stripComments(exec.output.code);
|
|
43
|
+
const codeLines = code.includes('\n') ? code.split('\n') : code.split('; ');
|
|
44
|
+
for (const codeLine of codeLines) {
|
|
45
|
+
const trimmed = codeLine.trim();
|
|
46
|
+
if (trimmed) {
|
|
47
|
+
lines.push(` ${trimmed}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lines.push('});');
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
saveCodeceptPlanToFile(plan: Plan): string {
|
|
57
|
+
const lines: string[] = [];
|
|
58
|
+
|
|
59
|
+
lines.push(`import step, { Section } from 'codeceptjs/steps';`);
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push(`Feature('${escapeString(plan.title)}')`);
|
|
62
|
+
lines.push('');
|
|
63
|
+
|
|
64
|
+
const startUrl = plan.url || plan.tests[0]?.startUrl;
|
|
65
|
+
if (startUrl) {
|
|
66
|
+
lines.push('Before(({ I }) => {');
|
|
67
|
+
lines.push(` I.amOnPage('${escapeString(startUrl)}');`);
|
|
68
|
+
lines.push(...this.getKnowledgeLines(startUrl));
|
|
69
|
+
lines.push('});');
|
|
70
|
+
lines.push('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const test of plan.tests) {
|
|
74
|
+
if (test.generatedCode) {
|
|
75
|
+
if (test.isSuccessful) {
|
|
76
|
+
lines.push(test.generatedCode);
|
|
77
|
+
} else {
|
|
78
|
+
lines.push(`// FAILED: ${test.scenario}`);
|
|
79
|
+
lines.push(test.generatedCode.replace(/Scenario\(/, 'Scenario.skip('));
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
lines.push(`Scenario.todo('${escapeString(test.scenario)}', ({ I }) => {`);
|
|
86
|
+
if (test.plannedSteps.length > 0) {
|
|
87
|
+
for (const step of test.plannedSteps) {
|
|
88
|
+
lines.push(` // ${step}`);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
lines.push(` // ${test.scenario}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push('});');
|
|
94
|
+
lines.push('');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const testsDir = ConfigParser.getInstance().getTestsDir();
|
|
98
|
+
mkdirSync(testsDir, { recursive: true });
|
|
99
|
+
|
|
100
|
+
const filename = plan.title.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
|
101
|
+
const filePath = join(testsDir, `${filename}.js`);
|
|
102
|
+
writeFileSync(filePath, lines.join('\n'));
|
|
103
|
+
this.savedFiles.add(filePath);
|
|
104
|
+
|
|
105
|
+
tag('substep').log(`Saved plan tests to: ${relativeToCwd(filePath)}`);
|
|
106
|
+
return filePath;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private getKnowledgeLines(url: string, indent = ' '): string[] {
|
|
110
|
+
const knowledgeTracker = new KnowledgeTracker();
|
|
111
|
+
const state = new ActionResult({ url });
|
|
112
|
+
const { wait, waitForElement, code } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement', 'code']);
|
|
113
|
+
|
|
114
|
+
const lines: string[] = [];
|
|
115
|
+
if (wait !== undefined) {
|
|
116
|
+
lines.push(`${indent}I.wait(${wait});`);
|
|
117
|
+
}
|
|
118
|
+
if (waitForElement) {
|
|
119
|
+
lines.push(`${indent}I.waitForElement(${JSON.stringify(waitForElement)});`);
|
|
120
|
+
}
|
|
121
|
+
if (code) {
|
|
122
|
+
for (const codeLine of code.split('\n')) {
|
|
123
|
+
const trimmed = codeLine.trim();
|
|
124
|
+
if (trimmed) lines.push(`${indent}${trimmed}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|