explorbot 0.5.10 → 0.6.0
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/bin/explorbot-cli.ts +494 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/dist/bin/explorbot-cli.js +455 -444
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/models.json +1 -1
- package/dist/package.json +2 -1
- package/dist/rules/navigator/output.md +2 -8
- package/dist/rules/navigator/verification-actions.md +6 -2
- package/dist/rules/planner/ignored-pages.md +1 -0
- package/dist/src/action-result.js +1 -1
- package/dist/src/action.d.ts +0 -1
- package/dist/src/action.js +0 -4
- package/dist/src/ai/captain/idle-mode.js +42 -26
- package/dist/src/ai/captain/mixin.d.ts +2 -0
- package/dist/src/ai/captain/web-mode.js +11 -0
- package/dist/src/ai/captain.d.ts +6 -4
- package/dist/src/ai/captain.js +25 -1
- package/dist/src/ai/driller.js +1 -1
- package/dist/src/ai/experience-compactor.js +2 -2
- package/dist/src/ai/judge.d.ts +7 -0
- package/dist/src/ai/judge.js +8 -1
- package/dist/src/ai/navigator.d.ts +1 -0
- package/dist/src/ai/navigator.js +11 -7
- package/dist/src/ai/planner/subpages.js +1 -1
- package/dist/src/ai/planner.d.ts +12 -5
- package/dist/src/ai/planner.js +138 -86
- package/dist/src/ai/provider.d.ts +1 -1
- package/dist/src/ai/provider.js +16 -5
- package/dist/src/ai/rerunner.js +1 -1
- package/dist/src/ai/researcher/cache.js +14 -6
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -2
- package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
- package/dist/src/ai/researcher.js +44 -24
- package/dist/src/ai/rules.js +6 -7
- package/dist/src/ai/scout.d.ts +12 -2
- package/dist/src/ai/scout.js +103 -8
- package/dist/src/ai/tester.d.ts +9 -5
- package/dist/src/ai/tester.js +22 -42
- package/dist/src/ai/tools.js +10 -8
- package/dist/src/application-spec.d.ts +2 -0
- package/dist/src/application-spec.js +17 -12
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/explore-command.d.ts +3 -2
- package/dist/src/commands/explore-command.js +35 -23
- package/dist/src/commands/help-json-command.js +3 -1
- package/dist/src/commands/init-command.js +4 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/plan-command.d.ts +3 -0
- package/dist/src/commands/plan-command.js +43 -17
- package/dist/src/commands/plan-save-command.js +6 -17
- package/dist/src/commands/plans-command.js +1 -1
- package/dist/src/commands/test-command.js +12 -2
- package/dist/src/components/App.d.ts +1 -2
- package/dist/src/components/App.js +1 -3
- package/dist/src/components/InputPane.js +0 -5
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +2 -12
- package/dist/src/explorbot.d.ts +3 -2
- package/dist/src/explorbot.js +32 -10
- package/dist/src/explorer.js +6 -7
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +18 -6
- package/dist/src/playwright-recorder.d.ts +0 -1
- package/dist/src/playwright-recorder.js +0 -2
- package/dist/src/reporter.d.ts +1 -1
- package/dist/src/reporter.js +3 -3
- package/dist/src/test-plan.js +1 -1
- package/dist/src/utils/error-page.d.ts +17 -0
- package/dist/src/utils/error-page.js +53 -2
- package/dist/src/utils/html-diff.d.ts +1 -2
- package/dist/src/utils/html-diff.js +4 -44
- package/dist/src/utils/logger.js +27 -10
- package/dist/src/utils/markdown-terminal.js +1 -2
- package/dist/src/utils/next-steps.js +20 -16
- package/dist/src/utils/page-readiness.d.ts +2 -1
- package/dist/src/utils/page-readiness.js +24 -5
- package/dist/src/utils/test-plan-markdown.d.ts +3 -0
- package/dist/src/utils/test-plan-markdown.js +35 -4
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +5 -0
- package/docs/basics/providers.md +1 -1
- package/docs/contributing/regression-tests.md +19 -4
- package/docs/reference/commands.md +6 -2
- package/docs/reference/configuration.md +1 -1
- package/docs/web-testing/agents.md +8 -0
- package/docs/web-testing/customization.md +2 -0
- package/docs/web-testing/researcher.md +1 -1
- package/models.json +1 -1
- package/package.json +2 -1
- package/rules/navigator/output.md +2 -8
- package/rules/navigator/verification-actions.md +6 -2
- package/rules/planner/ignored-pages.md +1 -0
- package/src/action-result.ts +1 -2
- package/src/action.ts +0 -5
- package/src/ai/captain/idle-mode.ts +38 -26
- package/src/ai/captain/mixin.ts +2 -1
- package/src/ai/captain/web-mode.ts +12 -0
- package/src/ai/captain.ts +25 -7
- package/src/ai/driller.ts +1 -2
- package/src/ai/experience-compactor.ts +2 -2
- package/src/ai/judge.ts +10 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner/subpages.ts +1 -2
- package/src/ai/planner.ts +140 -92
- package/src/ai/provider.ts +15 -6
- package/src/ai/rerunner.ts +1 -2
- package/src/ai/researcher/cache.ts +16 -7
- package/src/ai/researcher/deep-analysis.ts +7 -2
- package/src/ai/researcher/fingerprint-worker.ts +5 -5
- package/src/ai/researcher.ts +44 -28
- package/src/ai/rules.ts +6 -7
- package/src/ai/scout.ts +98 -10
- package/src/ai/tester.ts +29 -47
- package/src/ai/tools.ts +11 -9
- package/src/application-spec.ts +19 -13
- package/src/commands/clean-command.ts +1 -1
- package/src/commands/context-command.ts +0 -1
- package/src/commands/context-experience-command.ts +0 -1
- package/src/commands/context-knowledge-command.ts +0 -1
- package/src/commands/explore-command.ts +36 -23
- package/src/commands/help-json-command.ts +8 -6
- package/src/commands/init-command.ts +5 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/plan-command.ts +44 -17
- package/src/commands/plan-save-command.ts +6 -17
- package/src/commands/plans-command.ts +1 -1
- package/src/commands/test-command.ts +11 -2
- package/src/components/App.tsx +2 -6
- package/src/components/InputPane.tsx +0 -5
- package/src/config.ts +4 -16
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +31 -11
- package/src/explorer.ts +6 -7
- package/src/knowledge-tracker.ts +18 -7
- package/src/playwright-recorder.ts +0 -2
- package/src/reporter.ts +3 -4
- package/src/test-plan.ts +1 -2
- package/src/utils/error-page.ts +64 -2
- package/src/utils/html-diff.ts +4 -56
- package/src/utils/html.ts +0 -1
- package/src/utils/logger.ts +30 -12
- package/src/utils/markdown-terminal.ts +1 -2
- package/src/utils/next-steps.ts +19 -17
- package/src/utils/page-readiness.ts +33 -7
- package/src/utils/test-plan-markdown.ts +36 -4
- package/src/utils/url-matcher.ts +6 -0
package/dist/src/ai/tools.js
CHANGED
|
@@ -51,8 +51,8 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }, task) {
|
|
|
51
51
|
Never mix different elements — use separate click() calls instead.
|
|
52
52
|
REQUIRED: include at least one command WITHOUT a container — a wrong or stale container always fails.
|
|
53
53
|
Order by reliability:
|
|
54
|
-
1. I.click(
|
|
55
|
-
2. I.click(
|
|
54
|
+
1. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
|
|
55
|
+
2. I.click(text, container) - when the container is verified - e.g. I.click("Save", ".modal")
|
|
56
56
|
3. I.click(CSS, container) - e.g. I.click("#btn", ".modal")
|
|
57
57
|
4. I.click(CSS) or I.click(XPath) - when locator already includes context (ID, XPath)
|
|
58
58
|
After a result reporting multiple matches, reuse that locator with step.opts({ elementIndex: N }) as the last argument.
|
|
@@ -251,7 +251,7 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }, task) {
|
|
|
251
251
|
const normalizedKey = key.toLowerCase();
|
|
252
252
|
const matchingStandardKey = Array.from(standardKeys).find((sk) => sk.toLowerCase() === normalizedKey);
|
|
253
253
|
const isStandardKey = !!matchingStandardKey;
|
|
254
|
-
const keyToUse = matchingStandardKey || key;
|
|
254
|
+
const keyToUse = KEY_ALIASES[matchingStandardKey || ''] || matchingStandardKey || key;
|
|
255
255
|
if (!isSingleChar && !isStandardKey) {
|
|
256
256
|
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
257
257
|
const action = explorer.action();
|
|
@@ -405,7 +405,7 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }, task) {
|
|
|
405
405
|
commandsExecuted: lines.length,
|
|
406
406
|
attempts: action.executedSteps,
|
|
407
407
|
code: codeBlock,
|
|
408
|
-
suggestion: '
|
|
408
|
+
suggestion: 'Check the filled values with verify() before submitting, then submit with click().',
|
|
409
409
|
}, action);
|
|
410
410
|
}
|
|
411
411
|
catch (error) {
|
|
@@ -587,7 +587,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
587
587
|
inputSchema: z.object({
|
|
588
588
|
reason: z.string().describe('Why do you need fresh context? Required to prevent overuse.'),
|
|
589
589
|
}),
|
|
590
|
-
execute: async (
|
|
590
|
+
execute: async () => {
|
|
591
591
|
try {
|
|
592
592
|
const currentState = stateManager.getCurrentState();
|
|
593
593
|
if (!currentState) {
|
|
@@ -656,6 +656,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
656
656
|
});
|
|
657
657
|
}
|
|
658
658
|
return failedToolResult('verify', `Verification failed: ${assertion}`, {
|
|
659
|
+
attempts: result.results.map(({ code, passed, error }) => ({ code, passed, error })),
|
|
659
660
|
suggestion: 'The assertion could not be verified. Check if the condition is actually present on the page or try a different assertion.',
|
|
660
661
|
});
|
|
661
662
|
}
|
|
@@ -689,7 +690,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
689
690
|
inputSchema: z.object({
|
|
690
691
|
reason: z.string().describe('Why do you need research? What information is missing from existing context?'),
|
|
691
692
|
}),
|
|
692
|
-
execute: async (
|
|
693
|
+
execute: async () => {
|
|
693
694
|
try {
|
|
694
695
|
const currentState = stateManager.getCurrentState();
|
|
695
696
|
if (!currentState) {
|
|
@@ -795,7 +796,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
795
796
|
element: z.string().describe('Visual description of element to click (e.g., "blue Submit button in the modal footer", "Settings gear icon in top right")'),
|
|
796
797
|
context: z.string().describe('What you already tried and why it failed - helps with accurate identification'),
|
|
797
798
|
}),
|
|
798
|
-
execute: async ({ element
|
|
799
|
+
execute: async ({ element }) => {
|
|
799
800
|
if (Stats.visionDisabled) {
|
|
800
801
|
return failedToolResult('visualClick', 'Vision tools are disabled for this session. Use xpathCheck() to find the element, then click() with the discovered locator.');
|
|
801
802
|
}
|
|
@@ -929,7 +930,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
929
930
|
xpath: z.string().describe('XPath expression — use broad patterns to avoid missing the element'),
|
|
930
931
|
reason: z.string().describe('What element you are looking for and why'),
|
|
931
932
|
}),
|
|
932
|
-
execute: async ({ xpath
|
|
933
|
+
execute: async ({ xpath }) => {
|
|
933
934
|
const currentState = stateManager.getCurrentState();
|
|
934
935
|
if (!currentState) {
|
|
935
936
|
return failedToolResult('xpathCheck', 'No current page state available.');
|
|
@@ -1048,6 +1049,7 @@ const ARIA_OUTPUT_CAP = 4000;
|
|
|
1048
1049
|
const HTML_OUTPUT_CAP = 6000;
|
|
1049
1050
|
const ANALYSIS_OUTPUT_CAP = 2000;
|
|
1050
1051
|
const JUDGE_RECENT_ACTIONS_LIMIT = 8;
|
|
1052
|
+
const KEY_ALIASES = { Esc: 'Escape', Del: 'Delete' };
|
|
1051
1053
|
function cap(text, max) {
|
|
1052
1054
|
if (!text)
|
|
1053
1055
|
return '';
|
|
@@ -2,10 +2,12 @@ import { ActionResult } from './action-result.js';
|
|
|
2
2
|
export declare class ApplicationSpec {
|
|
3
3
|
pages: ApplicationSpecPage[];
|
|
4
4
|
readonly sourcePath: string;
|
|
5
|
+
totalMarkdownFiles: number;
|
|
5
6
|
constructor(sourcePath: string);
|
|
6
7
|
renderFor(state: ActionResult): string;
|
|
7
8
|
matchedUrls(state: ActionResult): string[];
|
|
8
9
|
get pageCount(): number;
|
|
10
|
+
get markdownFileCount(): number;
|
|
9
11
|
load(): void;
|
|
10
12
|
resolveSourcePath(sourcePath: string): string;
|
|
11
13
|
relevantPages(state: ActionResult): ApplicationSpecPage[];
|
|
@@ -8,6 +8,7 @@ import { loadMarkdownFiles } from "./utils/markdown-files.js";
|
|
|
8
8
|
export class ApplicationSpec {
|
|
9
9
|
pages = [];
|
|
10
10
|
sourcePath;
|
|
11
|
+
totalMarkdownFiles = 0;
|
|
11
12
|
constructor(sourcePath) {
|
|
12
13
|
this.sourcePath = this.resolveSourcePath(sourcePath);
|
|
13
14
|
this.load();
|
|
@@ -31,24 +32,23 @@ export class ApplicationSpec {
|
|
|
31
32
|
get pageCount() {
|
|
32
33
|
return this.pages.length;
|
|
33
34
|
}
|
|
35
|
+
get markdownFileCount() {
|
|
36
|
+
return this.totalMarkdownFiles;
|
|
37
|
+
}
|
|
34
38
|
load() {
|
|
35
39
|
if (!existsSync(this.sourcePath)) {
|
|
36
40
|
throw new Error(`Application spec not found: ${this.sourcePath}`);
|
|
37
41
|
}
|
|
38
42
|
const isDirectory = statSync(this.sourcePath).isDirectory();
|
|
39
|
-
if (!isDirectory && path.basename(this.sourcePath).toLowerCase() !== 'index.md') {
|
|
40
|
-
throw new Error(`Application spec file must be index.md: ${this.sourcePath}`);
|
|
41
|
-
}
|
|
42
43
|
const bundlePath = isDirectory ? this.sourcePath : path.dirname(this.sourcePath);
|
|
44
|
+
const allFiles = loadMarkdownFiles(bundlePath, { recursive: true });
|
|
43
45
|
const indexPath = path.join(bundlePath, 'index.md');
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
for (const file of loadMarkdownFiles(pagesPath, { recursive: true })) {
|
|
46
|
+
this.totalMarkdownFiles = allFiles.filter((file) => path.resolve(file.filePath) !== path.resolve(indexPath)).length;
|
|
47
|
+
for (const file of allFiles) {
|
|
48
|
+
if (path.resolve(file.filePath) === path.resolve(indexPath))
|
|
49
|
+
continue;
|
|
50
|
+
if (!file.data.url && file.data.format === undefined && file.data.version === undefined)
|
|
51
|
+
continue;
|
|
52
52
|
const parsed = APPLICATION_SPEC_PAGE_SCHEMA.safeParse(file.data);
|
|
53
53
|
if (!parsed.success && parsed.error.issues.some((issue) => issue.path[0] === 'format')) {
|
|
54
54
|
throw new Error(`Invalid application spec format in ${file.filePath}`);
|
|
@@ -61,8 +61,13 @@ export class ApplicationSpec {
|
|
|
61
61
|
}
|
|
62
62
|
this.pages.push({ url: parsed.data.url, content: file.content.trim() });
|
|
63
63
|
}
|
|
64
|
+
if (this.totalMarkdownFiles === 0) {
|
|
65
|
+
tag('warning').log(`No markdown files found in spec: ${bundlePath}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
64
68
|
if (this.pages.length === 0) {
|
|
65
|
-
|
|
69
|
+
tag('debug').log(`${this.totalMarkdownFiles} spec files found in ${bundlePath}`);
|
|
70
|
+
return;
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
resolveSourcePath(sourcePath) {
|
|
@@ -7,7 +7,6 @@ export class ContextExperienceCommand extends BaseCommand {
|
|
|
7
7
|
name = 'context:experience';
|
|
8
8
|
description = 'Print all matching experience for current page';
|
|
9
9
|
async execute(_args) {
|
|
10
|
-
const explorer = this.explorBot.getExplorer();
|
|
11
10
|
const state = this.explorBot.stateManager().getCurrentState();
|
|
12
11
|
if (!state) {
|
|
13
12
|
throw new Error('No active page');
|
|
@@ -7,7 +7,6 @@ export class ContextKnowledgeCommand extends BaseCommand {
|
|
|
7
7
|
name = 'context:knowledge';
|
|
8
8
|
description = 'Print all matching knowledge for current page';
|
|
9
9
|
async execute(_args) {
|
|
10
|
-
const explorer = this.explorBot.getExplorer();
|
|
11
10
|
const state = this.explorBot.stateManager().getCurrentState();
|
|
12
11
|
if (!state) {
|
|
13
12
|
throw new Error('No active page');
|
|
@@ -34,18 +34,19 @@ export declare class ExploreCommand extends BaseCommand {
|
|
|
34
34
|
discoverNewSubPages(mainPlan: Plan, mainUrl: string | undefined, styles: string[] | undefined, knownUrls: Set<string>): Promise<void>;
|
|
35
35
|
explorePage(url: string, feature: string | undefined, parentPlan: Plan | undefined, styles?: string[]): Promise<boolean>;
|
|
36
36
|
replanAndRun(pageUrl: string | undefined, feature: string | undefined, existingPlan: Plan, styles?: string[]): Promise<void>;
|
|
37
|
-
runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan,
|
|
37
|
+
runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, styles?: string[]): Promise<void>;
|
|
38
38
|
rememberCurrentPlan(): void;
|
|
39
39
|
planWithRetry(feature: string | undefined, opts: {
|
|
40
40
|
fresh: boolean;
|
|
41
41
|
style: string;
|
|
42
42
|
extend?: Plan;
|
|
43
|
-
completedPlans?: Plan[];
|
|
44
43
|
noSave?: boolean;
|
|
45
44
|
}, pageUrl?: string): Promise<void>;
|
|
45
|
+
printAddedTests(before: Set<Test>, style: string, pageUrl?: string): number;
|
|
46
46
|
parseConfigure(raw: string | undefined): ConfigureSpec;
|
|
47
47
|
printResults(): void;
|
|
48
48
|
printNextSteps(savedPlanPath?: string | null): void;
|
|
49
|
+
skipBlockedTest(test: Test, error: unknown): void;
|
|
49
50
|
isLimitReached(): boolean;
|
|
50
51
|
isDeadlineReached(): boolean;
|
|
51
52
|
runPendingTests(): Promise<void>;
|
|
@@ -6,7 +6,7 @@ import { Stats } from '../stats.js';
|
|
|
6
6
|
import { TestResult } from '../test-plan.js';
|
|
7
7
|
import { browserErrorMessage } from "../utils/browser-errors.js";
|
|
8
8
|
import { getCliName } from "../utils/cli-name.js";
|
|
9
|
-
import { ErrorPageError, getStateErrorPageError } from "../utils/error-page.js";
|
|
9
|
+
import { BlockedError, ErrorPageError, getStateErrorPageError } from "../utils/error-page.js";
|
|
10
10
|
import { tag } from '../utils/logger.js';
|
|
11
11
|
import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
|
|
12
12
|
import { keepSiteUrls, readSitemap } from "../utils/sitemap.js";
|
|
@@ -14,6 +14,7 @@ import { parseSpecPairs } from "../utils/spec.js";
|
|
|
14
14
|
import { safeFilename } from "../utils/strings.js";
|
|
15
15
|
import { UrlFilter } from "../utils/url-filter.js";
|
|
16
16
|
import { BaseCommand } from './base-command.js';
|
|
17
|
+
import { PlanCommand } from './plan-command.js';
|
|
17
18
|
const MAX_SUB_PAGE_ATTEMPTS = 30;
|
|
18
19
|
const PRIORITY_ORDER = { critical: 0, important: 1, high: 2, normal: 3, low: 4 };
|
|
19
20
|
export const DEADLINE_RESERVE_MS = 3 * 60_000;
|
|
@@ -135,7 +136,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
135
136
|
tag('multiline').log(lines.join('\n'), { maxLines: 24 });
|
|
136
137
|
}
|
|
137
138
|
async runFreshMode(mainUrl, feature, styles) {
|
|
138
|
-
await this.runAllStyles(mainUrl, feature, undefined,
|
|
139
|
+
await this.runAllStyles(mainUrl, feature, undefined, styles);
|
|
139
140
|
this.rememberCurrentPlan();
|
|
140
141
|
const mainPlan = this.explorBot.getCurrentPlan();
|
|
141
142
|
if (!mainPlan)
|
|
@@ -319,7 +320,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
319
320
|
tag('warning').log(`Skipping page: ${errorPage.message}`);
|
|
320
321
|
return false;
|
|
321
322
|
}
|
|
322
|
-
await this.runAllStyles(url, feature, parentPlan,
|
|
323
|
+
await this.runAllStyles(url, feature, parentPlan, styles);
|
|
323
324
|
const plan = this.explorBot.getCurrentPlan();
|
|
324
325
|
if (plan?.tests.length && !this.completedPlans.includes(plan)) {
|
|
325
326
|
this.completedPlans.push(plan);
|
|
@@ -332,14 +333,14 @@ export class ExploreCommand extends BaseCommand {
|
|
|
332
333
|
if (this.isLimitReached())
|
|
333
334
|
break;
|
|
334
335
|
this.explorBot.setCurrentPlan(existingPlan);
|
|
335
|
-
const opts = { fresh: false, style
|
|
336
|
+
const opts = { fresh: false, style };
|
|
336
337
|
if (this.dryRun)
|
|
337
338
|
opts.noSave = true;
|
|
338
339
|
await this.planWithRetry(feature, opts, pageUrl);
|
|
339
340
|
await this.runPendingTests();
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
|
-
async runAllStyles(pageUrl, feature, parentPlan,
|
|
343
|
+
async runAllStyles(pageUrl, feature, parentPlan, styles) {
|
|
343
344
|
const styleList = styles ?? Object.keys(getStyles());
|
|
344
345
|
let fresh = true;
|
|
345
346
|
for (const style of styleList) {
|
|
@@ -348,7 +349,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
348
349
|
if (!fresh && pageUrl && !this.dryRun) {
|
|
349
350
|
await this.explorBot.visit(pageUrl);
|
|
350
351
|
}
|
|
351
|
-
const opts = { fresh, style
|
|
352
|
+
const opts = { fresh, style };
|
|
352
353
|
if (fresh && parentPlan)
|
|
353
354
|
opts.extend = parentPlan;
|
|
354
355
|
if (this.dryRun)
|
|
@@ -377,6 +378,10 @@ export class ExploreCommand extends BaseCommand {
|
|
|
377
378
|
async planWithRetry(feature, opts, pageUrl) {
|
|
378
379
|
const before = new Set(this.explorBot.getCurrentPlan()?.tests ?? []);
|
|
379
380
|
await this.explorBot.plan(feature, opts);
|
|
381
|
+
if (this.explorBot.lastPlanError instanceof BlockedError) {
|
|
382
|
+
tag('warning').log(`Skipping page: ${this.explorBot.lastPlanError.message}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
380
385
|
if (this.explorBot.lastPlanError) {
|
|
381
386
|
if (this.explorBot.lastPlanError instanceof ErrorPageError) {
|
|
382
387
|
throw this.explorBot.lastPlanError;
|
|
@@ -390,14 +395,15 @@ export class ExploreCommand extends BaseCommand {
|
|
|
390
395
|
return;
|
|
391
396
|
}
|
|
392
397
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const added =
|
|
398
|
+
this.printAddedTests(before, opts.style, pageUrl);
|
|
399
|
+
}
|
|
400
|
+
printAddedTests(before, style, pageUrl) {
|
|
401
|
+
const added = this.explorBot.getCurrentPlan()?.tests.filter((t) => !before.has(t)) ?? [];
|
|
397
402
|
if (added.length === 0)
|
|
398
|
-
return;
|
|
403
|
+
return 0;
|
|
399
404
|
const urlNote = pageUrl ? ` for ${pageUrl}` : '';
|
|
400
|
-
this.printPreview(`Planner added ${added.length} new test(s) [style=${
|
|
405
|
+
this.printPreview(`Planner added ${added.length} new test(s) [style=${style}]${urlNote}:`, added);
|
|
406
|
+
return added.length;
|
|
401
407
|
}
|
|
402
408
|
parseConfigure(raw) {
|
|
403
409
|
const cfg = { enabled: false, newRatio: 1.0 };
|
|
@@ -542,16 +548,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
542
548
|
const cli = getCliName();
|
|
543
549
|
const sections = [];
|
|
544
550
|
if (savedPlanPath) {
|
|
545
|
-
|
|
546
|
-
sections.push({
|
|
547
|
-
label: 'Plan',
|
|
548
|
-
path: savedPlanPath,
|
|
549
|
-
commands: [
|
|
550
|
-
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
551
|
-
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
552
|
-
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
553
|
-
],
|
|
554
|
-
});
|
|
551
|
+
sections.push(PlanCommand.planSection(savedPlanPath));
|
|
555
552
|
}
|
|
556
553
|
const savedFiles = this.explorBot.agentHistorian().getSavedFiles();
|
|
557
554
|
const screencasts = savedFiles.filter((f) => f.endsWith('.webm'));
|
|
@@ -585,6 +582,14 @@ export class ExploreCommand extends BaseCommand {
|
|
|
585
582
|
});
|
|
586
583
|
printNextSteps(sections);
|
|
587
584
|
}
|
|
585
|
+
skipBlockedTest(test, error) {
|
|
586
|
+
if (!(error instanceof BlockedError))
|
|
587
|
+
throw error;
|
|
588
|
+
tag('warning').log(`Skipping test: ${error.message}`);
|
|
589
|
+
test.start();
|
|
590
|
+
test.addNote(error.message, TestResult.SKIPPED);
|
|
591
|
+
test.finish(TestResult.SKIPPED);
|
|
592
|
+
}
|
|
588
593
|
isLimitReached() {
|
|
589
594
|
if (this.maxTests != null && this.testsRun >= this.maxTests)
|
|
590
595
|
return true;
|
|
@@ -623,7 +628,14 @@ export class ExploreCommand extends BaseCommand {
|
|
|
623
628
|
test.finish(TestResult.SKIPPED);
|
|
624
629
|
}
|
|
625
630
|
else {
|
|
626
|
-
|
|
631
|
+
const run = () => this.explorBot.agentTester().test(test, { deadline: this.hardDeadlineAt });
|
|
632
|
+
await run()
|
|
633
|
+
.catch(async (err) => {
|
|
634
|
+
if (!(await this.explorBot.agentCaptain().resolveBlocker(err, () => this.explorBot.agentNavigator().isOn(test.startUrl))))
|
|
635
|
+
throw err;
|
|
636
|
+
return run();
|
|
637
|
+
})
|
|
638
|
+
.catch((err) => this.skipBlockedTest(test, err));
|
|
627
639
|
}
|
|
628
640
|
this.testsRun++;
|
|
629
641
|
}
|
|
@@ -27,7 +27,9 @@ export class HelpJsonCommand {
|
|
|
27
27
|
required: argument.required,
|
|
28
28
|
variadic: argument.variadic,
|
|
29
29
|
})),
|
|
30
|
-
options:
|
|
30
|
+
options: cmd.options
|
|
31
|
+
.filter((option) => !option.hidden)
|
|
32
|
+
.map((option) => ({
|
|
31
33
|
flags: option.flags,
|
|
32
34
|
description: option.description,
|
|
33
35
|
default: option.defaultValue,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
3
|
import figureSet from 'figures';
|
|
5
4
|
import { ConfigParser, PROVIDERS, missingModelRoles } from "../config.js";
|
|
6
5
|
import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from "../global-config.js";
|
|
@@ -299,10 +298,11 @@ function printInitNextSteps(options) {
|
|
|
299
298
|
label: 'Docs',
|
|
300
299
|
commands: [{ command: 'https://testomat.ai/docs/explorbot' }],
|
|
301
300
|
});
|
|
301
|
+
sections.push({
|
|
302
|
+
label: 'Add knowledge (logins, credentials) — created on first use in ./knowledge',
|
|
303
|
+
commands: [{ command: `${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'` }],
|
|
304
|
+
});
|
|
302
305
|
printNextSteps(sections);
|
|
303
|
-
log('');
|
|
304
|
-
log('Add knowledge (logins, credentials) — created on first use in ./knowledge:');
|
|
305
|
-
tag('substep').log(chalk.yellow(`${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
306
306
|
tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
|
|
307
307
|
}
|
|
308
308
|
function writeEnvKey(key, value, envPath = globalEnvPath()) {
|
|
@@ -10,7 +10,6 @@ export class LearnCommand extends BaseCommand {
|
|
|
10
10
|
const note = args.trim();
|
|
11
11
|
if (!note) {
|
|
12
12
|
const AddKnowledge = (await import('../components/AddKnowledge.js')).default;
|
|
13
|
-
const explorer = this.explorBot.getExplorer();
|
|
14
13
|
const state = this.explorBot.stateManager().getCurrentState();
|
|
15
14
|
const initialUrl = state?.url || '';
|
|
16
15
|
const { unmount } = render(React.createElement(AddKnowledge, {
|
|
@@ -24,7 +23,6 @@ export class LearnCommand extends BaseCommand {
|
|
|
24
23
|
});
|
|
25
24
|
return;
|
|
26
25
|
}
|
|
27
|
-
const explorer = this.explorBot.getExplorer();
|
|
28
26
|
const state = this.explorBot.stateManager().getCurrentState();
|
|
29
27
|
if (!state) {
|
|
30
28
|
throw new Error('No active page to attach knowledge');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NextStepSection } from '../utils/next-steps.js';
|
|
1
2
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
2
3
|
export declare class PlanCommand extends BaseCommand {
|
|
3
4
|
name: string;
|
|
@@ -8,7 +9,9 @@ export declare class PlanCommand extends BaseCommand {
|
|
|
8
9
|
description: string;
|
|
9
10
|
}[];
|
|
10
11
|
execute(args: string): Promise<void>;
|
|
12
|
+
static planSection(planPath: string, cliSuffix?: string): NextStepSection;
|
|
11
13
|
printNextSteps(): void;
|
|
14
|
+
cliSuffix(): string;
|
|
12
15
|
printPlanSummary(): void;
|
|
13
16
|
updateSuggestions(): void;
|
|
14
17
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import figureSet from 'figures';
|
|
@@ -16,8 +17,12 @@ export class PlanCommand extends BaseCommand {
|
|
|
16
17
|
options = [
|
|
17
18
|
{ flags: '--fresh', description: 'Regenerate plan from scratch' },
|
|
18
19
|
{ flags: '--clear', description: 'Clear plan before regenerating' },
|
|
20
|
+
{ flags: '--append', description: 'Add tests to existing plan file' },
|
|
19
21
|
{ flags: '--style <name>', description: 'Planning style (normal, curious, psycho, performer)' },
|
|
20
22
|
{ flags: '--focus <feature>', description: 'Focus area for test planning' },
|
|
23
|
+
{ flags: '--tests <count>', description: 'Exact number of tests to generate' },
|
|
24
|
+
{ flags: '--min-tests <count>', description: 'Minimum number of tests to generate' },
|
|
25
|
+
{ flags: '--max-tests <count>', description: 'Maximum number of tests to generate' },
|
|
21
26
|
];
|
|
22
27
|
async execute(args) {
|
|
23
28
|
const { opts, args: remaining } = this.parseArgs(args);
|
|
@@ -26,10 +31,18 @@ export class PlanCommand extends BaseCommand {
|
|
|
26
31
|
this.explorBot.clearPlan();
|
|
27
32
|
tag('success').log('Plan cleared');
|
|
28
33
|
}
|
|
34
|
+
if (opts.append) {
|
|
35
|
+
const existingPlanPath = path.join(this.explorBot.getPlansDir(), this.explorBot.generatePlanFilename());
|
|
36
|
+
if (existsSync(existingPlanPath))
|
|
37
|
+
this.explorBot.loadPlan(existingPlanPath);
|
|
38
|
+
}
|
|
29
39
|
if (focus) {
|
|
30
40
|
tag('info').log(`Planning focus: ${focus}`);
|
|
31
41
|
}
|
|
32
|
-
|
|
42
|
+
const testsCount = opts.tests ? Number.parseInt(opts.tests, 10) : undefined;
|
|
43
|
+
const minTests = testsCount ?? (opts.minTests ? Number.parseInt(opts.minTests, 10) : undefined);
|
|
44
|
+
const maxTests = testsCount ?? (opts.maxTests ? Number.parseInt(opts.maxTests, 10) : undefined);
|
|
45
|
+
await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style, minTests, maxTests });
|
|
33
46
|
const plan = this.explorBot.getCurrentPlan();
|
|
34
47
|
if (!plan?.tests.length) {
|
|
35
48
|
throw new Error('No test scenarios in the current plan.');
|
|
@@ -38,28 +51,32 @@ export class PlanCommand extends BaseCommand {
|
|
|
38
51
|
this.updateSuggestions();
|
|
39
52
|
this.printNextSteps();
|
|
40
53
|
}
|
|
54
|
+
static planSection(planPath, cliSuffix = '') {
|
|
55
|
+
const cli = getCliName();
|
|
56
|
+
const planName = path.basename(planPath, '.md');
|
|
57
|
+
return {
|
|
58
|
+
label: 'Plan',
|
|
59
|
+
path: planPath,
|
|
60
|
+
commands: [
|
|
61
|
+
{ label: 'Run all tests', command: `${cli} test ${planName}${cliSuffix}` },
|
|
62
|
+
{ label: 'Run one test', command: `${cli} test ${planName} 1${cliSuffix}` },
|
|
63
|
+
{ label: 'Run a range', command: `${cli} test ${planName} 1-3${cliSuffix}` },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
41
67
|
printNextSteps() {
|
|
42
68
|
const savedPath = this.explorBot.lastSavedPlanPath;
|
|
43
69
|
if (!savedPath)
|
|
44
70
|
return;
|
|
45
71
|
const cli = getCliName();
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
path: savedPath,
|
|
51
|
-
commands: [
|
|
52
|
-
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
53
|
-
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
54
|
-
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
55
|
-
{ label: 'Reload', command: `/plan:load ${relPlan}` },
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
];
|
|
72
|
+
const cliSuffix = this.cliSuffix();
|
|
73
|
+
const section = PlanCommand.planSection(savedPath, cliSuffix);
|
|
74
|
+
section.commands?.push({ label: 'Reload', command: `/plan:load ${path.basename(savedPath, '.md')}` });
|
|
75
|
+
const sections = [section];
|
|
59
76
|
const suite = this.explorBot.getSuite();
|
|
60
77
|
const files = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
|
|
61
78
|
if (files.length > 0) {
|
|
62
|
-
const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
|
|
79
|
+
const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
|
|
63
80
|
sections.push({
|
|
64
81
|
label: `Automated tests (${files.length})`,
|
|
65
82
|
commands,
|
|
@@ -67,6 +84,15 @@ export class PlanCommand extends BaseCommand {
|
|
|
67
84
|
}
|
|
68
85
|
printNextSteps(sections);
|
|
69
86
|
}
|
|
87
|
+
cliSuffix() {
|
|
88
|
+
const options = this.explorBot.getOptions();
|
|
89
|
+
let suffix = '';
|
|
90
|
+
if (options.path)
|
|
91
|
+
suffix += ` --path ${options.path}`;
|
|
92
|
+
if (options.session)
|
|
93
|
+
suffix += ' --session';
|
|
94
|
+
return suffix;
|
|
95
|
+
}
|
|
70
96
|
printPlanSummary() {
|
|
71
97
|
const suite = this.explorBot.getSuite();
|
|
72
98
|
const plan = this.explorBot.getCurrentPlan();
|
|
@@ -78,10 +104,10 @@ export class PlanCommand extends BaseCommand {
|
|
|
78
104
|
}
|
|
79
105
|
}
|
|
80
106
|
if (plan?.tests.length) {
|
|
81
|
-
console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
|
|
107
|
+
console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}\n`);
|
|
82
108
|
for (let i = 0; i < plan.tests.length; i++) {
|
|
83
109
|
const t = plan.tests[i];
|
|
84
|
-
console.log(` ${chalk.
|
|
110
|
+
console.log(` ${chalk.cyan(`${i + 1}.`)} ${t.scenario}`);
|
|
85
111
|
}
|
|
86
112
|
}
|
|
87
113
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { printNextSteps
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { printNextSteps } from "../utils/next-steps.js";
|
|
3
|
+
import { PlanCommand } from './plan-command.js';
|
|
3
4
|
import { BaseCommand } from './base-command.js';
|
|
4
5
|
export class PlanSaveCommand extends BaseCommand {
|
|
5
6
|
name = 'plan:save';
|
|
@@ -14,20 +15,8 @@ export class PlanSaveCommand extends BaseCommand {
|
|
|
14
15
|
const savedPath = this.explorBot.savePlan(filename);
|
|
15
16
|
if (!savedPath)
|
|
16
17
|
return;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
label: 'Plan',
|
|
22
|
-
path: savedPath,
|
|
23
|
-
commands: [
|
|
24
|
-
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
25
|
-
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
26
|
-
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
27
|
-
{ label: 'Reload', command: `/plan:load ${relPlan}` },
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
];
|
|
31
|
-
printNextSteps(sections);
|
|
18
|
+
const section = PlanCommand.planSection(savedPath);
|
|
19
|
+
section.commands?.push({ label: 'Reload', command: `/plan:load ${path.basename(savedPath, '.md')}` });
|
|
20
|
+
printNextSteps([section]);
|
|
32
21
|
}
|
|
33
22
|
}
|
|
@@ -44,7 +44,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
44
44
|
}
|
|
45
45
|
tag('info').log('');
|
|
46
46
|
tag('info').log('Run test from this plan as:');
|
|
47
|
-
tag('info').log(`${getCliName()} test
|
|
47
|
+
tag('info').log(`${getCliName()} test ${file.name} 1`);
|
|
48
48
|
}
|
|
49
49
|
resolvePlanFile(target, files) {
|
|
50
50
|
const index = Number.parseInt(target, 10);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Stats } from '../stats.js';
|
|
2
2
|
import { Plan, Test } from '../test-plan.js';
|
|
3
|
+
import { BlockedError } from "../utils/error-page.js";
|
|
3
4
|
import { tag } from '../utils/logger.js';
|
|
4
5
|
import { BaseCommand } from './base-command.js';
|
|
5
6
|
export class TestCommand extends BaseCommand {
|
|
@@ -15,6 +16,11 @@ export class TestCommand extends BaseCommand {
|
|
|
15
16
|
if (opts.fromPlan) {
|
|
16
17
|
this.explorBot.loadPlan(String(opts.fromPlan));
|
|
17
18
|
}
|
|
19
|
+
if (!opts.fromPlan && remaining[0] && Plan.resolveFile(remaining[0], this.explorBot.getPlansDir())) {
|
|
20
|
+
this.explorBot.loadPlan(remaining.shift());
|
|
21
|
+
if (remaining.length === 0)
|
|
22
|
+
remaining.push('*');
|
|
23
|
+
}
|
|
18
24
|
const plan = this.explorBot.getCurrentPlan();
|
|
19
25
|
const selector = remaining.join(' ');
|
|
20
26
|
Stats.mode = 'test';
|
|
@@ -70,12 +76,16 @@ export class TestCommand extends BaseCommand {
|
|
|
70
76
|
const tester = this.explorBot.agentTester();
|
|
71
77
|
for (const [index, test] of toExecute.entries()) {
|
|
72
78
|
tag('info').log(`Starting test ${index + 1}/${toExecute.length}: ${test.scenario}`);
|
|
73
|
-
await tester.test(test)
|
|
79
|
+
await tester.test(test).catch((err) => {
|
|
80
|
+
if (!(err instanceof BlockedError))
|
|
81
|
+
throw err;
|
|
82
|
+
tag('warning').log(`Skipping test: ${err.message}`);
|
|
83
|
+
});
|
|
74
84
|
}
|
|
75
85
|
tag('success').log('Test execution finished');
|
|
76
86
|
}
|
|
77
87
|
completeArguments() {
|
|
78
|
-
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value:
|
|
88
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
|
|
79
89
|
}
|
|
80
90
|
}
|
|
81
91
|
function parseTestIndices(input, total) {
|
|
@@ -3,7 +3,6 @@ import type { ExplorBot } from '../explorbot.js';
|
|
|
3
3
|
interface AppProps {
|
|
4
4
|
explorBot: ExplorBot;
|
|
5
5
|
initialShowInput?: boolean;
|
|
6
|
-
exitOnEmptyInput?: boolean;
|
|
7
6
|
}
|
|
8
|
-
export declare function App({ explorBot, initialShowInput
|
|
7
|
+
export declare function App({ explorBot, initialShowInput }: AppProps): React.JSX.Element;
|
|
9
8
|
export {};
|
|
@@ -13,14 +13,13 @@ import StateTransitionPane from './StateTransitionPane.js';
|
|
|
13
13
|
import TaskPane, { WINDOW_SIZE } from './TaskPane.js';
|
|
14
14
|
import WelcomeChecklist from './WelcomeChecklist.js';
|
|
15
15
|
import WelcomeCommands from './WelcomeCommands.js';
|
|
16
|
-
export function App({ explorBot, initialShowInput = false
|
|
16
|
+
export function App({ explorBot, initialShowInput = false }) {
|
|
17
17
|
const sessionStartedAtRef = useRef(Date.now());
|
|
18
18
|
const InputComponent = InputReadline;
|
|
19
19
|
const [showSessionTimer, setShowSessionTimer] = useState(false);
|
|
20
20
|
const [showInput, setShowInput] = useState(initialShowInput);
|
|
21
21
|
const [interruptPrompt, setInterruptPrompt] = useState(null);
|
|
22
22
|
const [currentState, setCurrentState] = useState(null);
|
|
23
|
-
const [lastTransition, setLastTransition] = useState(null);
|
|
24
23
|
const [tasks, setTasks] = useState([]);
|
|
25
24
|
const tasksRef = useRef([]);
|
|
26
25
|
const [showPlanEditor, setShowPlanEditor] = useState(false);
|
|
@@ -50,7 +49,6 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
50
49
|
const manager = explorBot.stateManager();
|
|
51
50
|
unsubscribe = manager.onStateChange((transition) => {
|
|
52
51
|
if (mounted) {
|
|
53
|
-
setLastTransition(transition);
|
|
54
52
|
setCurrentState(transition.toState);
|
|
55
53
|
}
|
|
56
54
|
});
|