explorbot 0.1.32-beta.1 → 0.2.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/README.md +14 -14
- package/bin/explorbot-cli.ts +4 -3
- package/boat/doc-collector/src/ai/documentarian.ts +22 -27
- package/boat/doc-collector/src/ai/tools.ts +67 -10
- package/boat/doc-collector/src/docbot.ts +28 -7
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +33 -1
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
- package/dist/boat/doc-collector/src/ai/tools.js +47 -10
- package/dist/boat/doc-collector/src/docbot.js +25 -6
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +29 -0
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/package.json +20 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +3 -10
- package/dist/src/action.js +18 -111
- package/dist/src/ai/captain/web-mode.js +0 -7
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +4 -19
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +0 -4
- package/dist/src/ai/driller.js +3 -12
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +3 -2
- package/dist/src/ai/historian/playwright.d.ts +2 -0
- package/dist/src/ai/historian/playwright.js +3 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +5 -4
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +3 -5
- package/dist/src/ai/navigator.js +11 -46
- package/dist/src/ai/pilot.d.ts +1 -3
- package/dist/src/ai/pilot.js +6 -17
- package/dist/src/ai/planner.d.ts +3 -3
- package/dist/src/ai/planner.js +49 -37
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -136
- package/dist/src/ai/quartermaster.d.ts +21 -22
- package/dist/src/ai/quartermaster.js +4 -10
- package/dist/src/ai/rerunner.d.ts +1 -2
- package/dist/src/ai/rerunner.js +14 -22
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/deep-analysis.js +3 -6
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.js +5 -1
- package/dist/src/ai/researcher.js +12 -53
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +1 -1
- package/dist/src/ai/task-agent.js +2 -27
- package/dist/src/ai/tester.d.ts +0 -2
- package/dist/src/ai/tester.js +10 -61
- package/dist/src/ai/tools.d.ts +18 -7
- package/dist/src/ai/tools.js +119 -101
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-command.js +1 -1
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +3 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +2 -2
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +81 -154
- package/dist/src/explorbot.d.ts +4 -5
- package/dist/src/explorbot.js +17 -51
- package/dist/src/explorer.d.ts +4 -15
- package/dist/src/explorer.js +18 -68
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +6 -31
- package/dist/src/state-manager.js +10 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- package/dist/src/utils/web-element.d.ts +2 -1
- package/dist/src/utils/web-element.js +3 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/package.json +20 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +16 -130
- package/src/ai/captain/web-mode.ts +0 -7
- package/src/ai/captain.ts +5 -17
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +3 -16
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +4 -2
- package/src/ai/historian/playwright.ts +4 -2
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +4 -7
- package/src/ai/navigator.ts +13 -49
- package/src/ai/pilot.ts +6 -16
- package/src/ai/planner.ts +56 -44
- package/src/ai/provider.ts +85 -145
- package/src/ai/quartermaster.ts +34 -41
- package/src/ai/rerunner.ts +17 -21
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/deep-analysis.ts +3 -6
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +5 -1
- package/src/ai/researcher.ts +11 -59
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +3 -30
- package/src/ai/tester.ts +12 -73
- package/src/ai/tools.ts +119 -106
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-command.ts +1 -1
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +3 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +2 -2
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +88 -151
- package/src/explorbot.ts +17 -51
- package/src/explorer.ts +20 -72
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +15 -156
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- package/src/utils/web-element.ts +4 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
|
@@ -136,7 +136,7 @@ function extractKeyFields(body, result = {}, depth = 0) {
|
|
|
136
136
|
return result;
|
|
137
137
|
}
|
|
138
138
|
for (const [key, value] of Object.entries(body)) {
|
|
139
|
-
if (value
|
|
139
|
+
if (value == null)
|
|
140
140
|
continue;
|
|
141
141
|
if (typeof value === 'object') {
|
|
142
142
|
extractKeyFields(value, result, depth + 1);
|
|
@@ -18,7 +18,6 @@ export declare class Fisherman implements Agent {
|
|
|
18
18
|
hasApiConfig: boolean;
|
|
19
19
|
constructor(provider: Provider, apiClient: ApiClient, requestStore: RequestStore, specLoader: () => Promise<any | null>, baseEndpoint: string, cookieProvider: () => Promise<Record<string, string>>, configHeaders?: Record<string, string>, hasApiConfig?: boolean);
|
|
20
20
|
isAvailable(): boolean;
|
|
21
|
-
getMode(): string;
|
|
22
21
|
ensureReady(scopeUrl?: string): Promise<void>;
|
|
23
22
|
getEndpointList(scopeUrl?: string): string;
|
|
24
23
|
prepareData(instructions: string, scopeUrl?: string, sessionName?: string): Promise<FishermanResult>;
|
package/dist/src/ai/fisherman.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StateManager } from '../../state-manager.js';
|
|
1
2
|
import type { Plan } from '../../test-plan.js';
|
|
2
3
|
import type { Conversation } from '../conversation.js';
|
|
3
4
|
import type { Constructor } from './mixin.js';
|
|
@@ -8,6 +9,7 @@ export interface CodeceptJSMethods {
|
|
|
8
9
|
export declare function WithCodeceptJS<T extends Constructor>(Base: T): {
|
|
9
10
|
new (...args: any[]): {
|
|
10
11
|
savedFiles: Set<string>;
|
|
12
|
+
stateManager?: StateManager;
|
|
11
13
|
toCode(conversation: Conversation, scenario: string): string;
|
|
12
14
|
saveCodeceptPlanToFile(plan: Plan): string;
|
|
13
15
|
getKnowledgeLines(url: string, indent?: string): string[];
|
|
@@ -2,7 +2,6 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ActionResult } from "../../action-result.js";
|
|
4
4
|
import { ConfigParser } from "../../config.js";
|
|
5
|
-
import { KnowledgeTracker } from "../../knowledge-tracker.js";
|
|
6
5
|
import { tag } from "../../utils/logger.js";
|
|
7
6
|
import { relativeToCwd } from "../../utils/next-steps.js";
|
|
8
7
|
import { safeFilename } from "../../utils/strings.js";
|
|
@@ -87,7 +86,9 @@ export function WithCodeceptJS(Base) {
|
|
|
87
86
|
return filePath;
|
|
88
87
|
}
|
|
89
88
|
getKnowledgeLines(url, indent = ' ') {
|
|
90
|
-
const knowledgeTracker =
|
|
89
|
+
const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
|
|
90
|
+
if (!knowledgeTracker)
|
|
91
|
+
return [];
|
|
91
92
|
const state = new ActionResult({ url });
|
|
92
93
|
const { wait, waitForElement, code } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement', 'code']);
|
|
93
94
|
const lines = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StateManager } from '../../state-manager.js';
|
|
1
2
|
import { type PlaywrightRecorder } from '../../playwright-recorder.js';
|
|
2
3
|
import type { Plan } from '../../test-plan.js';
|
|
3
4
|
import type { Conversation } from '../conversation.js';
|
|
@@ -13,6 +14,7 @@ export declare function WithPlaywright<T extends Constructor>(Base: T): {
|
|
|
13
14
|
helper: any;
|
|
14
15
|
} | undefined;
|
|
15
16
|
savedFiles: Set<string>;
|
|
17
|
+
stateManager?: StateManager;
|
|
16
18
|
toPlaywrightCode(conversation: Conversation, scenario: string): Promise<string>;
|
|
17
19
|
savePlaywrightPlanToFile(plan: Plan): string;
|
|
18
20
|
getPlaywrightKnowledgeLines(url: string, indent?: string): string[];
|
|
@@ -2,7 +2,6 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ActionResult } from "../../action-result.js";
|
|
4
4
|
import { ConfigParser } from "../../config.js";
|
|
5
|
-
import { KnowledgeTracker } from "../../knowledge-tracker.js";
|
|
6
5
|
import { renderAssertion, renderCall } from "../../playwright-recorder.js";
|
|
7
6
|
import { tag } from "../../utils/logger.js";
|
|
8
7
|
import { relativeToCwd } from "../../utils/next-steps.js";
|
|
@@ -124,7 +123,9 @@ export function WithPlaywright(Base) {
|
|
|
124
123
|
return filePath;
|
|
125
124
|
}
|
|
126
125
|
getPlaywrightKnowledgeLines(url, indent = ' ') {
|
|
127
|
-
const knowledgeTracker =
|
|
126
|
+
const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
|
|
127
|
+
if (!knowledgeTracker)
|
|
128
|
+
return [];
|
|
128
129
|
const state = new ActionResult({ url });
|
|
129
130
|
const { wait, waitForElement } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement']);
|
|
130
131
|
const lines = [];
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { ToolExecution } from '../conversation.js';
|
|
2
|
-
export { isNonReusableCode, stripComments } from '../../utils/step-analyzer.js';
|
|
3
2
|
export declare function escapeString(str: string): string;
|
|
4
3
|
export declare function getExecutionLabel(exec: ToolExecution, fallback?: string): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExplorbotConfig } from '../config.js';
|
|
2
|
-
import { ExperienceTracker } from '../experience-tracker.js';
|
|
2
|
+
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
3
3
|
import type { PlaywrightRecorder } from '../playwright-recorder.js';
|
|
4
4
|
import type { Reporter } from '../reporter.js';
|
|
5
5
|
import type { StateManager } from '../state-manager.js';
|
|
@@ -9,7 +9,6 @@ import { type ExperienceMethods } from './historian/experience.js';
|
|
|
9
9
|
import { type PlaywrightMethods } from './historian/playwright.js';
|
|
10
10
|
import { type ScreencastMethods } from './historian/screencast.js';
|
|
11
11
|
import type { Provider } from './provider.js';
|
|
12
|
-
export { isNonReusableCode } from '../utils/step-analyzer.js';
|
|
13
12
|
declare const HistorianBase: {
|
|
14
13
|
new (...args: any[]): {
|
|
15
14
|
config: ExplorbotConfig | undefined;
|
|
@@ -40,6 +39,7 @@ declare const HistorianBase: {
|
|
|
40
39
|
helper: any;
|
|
41
40
|
};
|
|
42
41
|
savedFiles: Set<string>;
|
|
42
|
+
stateManager?: StateManager;
|
|
43
43
|
toPlaywrightCode(conversation: import("./conversation.js").Conversation, scenario: string): Promise<string>;
|
|
44
44
|
savePlaywrightPlanToFile(plan: Plan): string;
|
|
45
45
|
getPlaywrightKnowledgeLines(url: string, indent?: string): string[];
|
|
@@ -47,6 +47,7 @@ declare const HistorianBase: {
|
|
|
47
47
|
} & {
|
|
48
48
|
new (...args: any[]): {
|
|
49
49
|
savedFiles: Set<string>;
|
|
50
|
+
stateManager?: StateManager;
|
|
50
51
|
toCode(conversation: import("./conversation.js").Conversation, scenario: string): string;
|
|
51
52
|
saveCodeceptPlanToFile(plan: Plan): string;
|
|
52
53
|
getKnowledgeLines(url: string, indent?: string): string[];
|
|
@@ -93,7 +94,7 @@ export declare class Historian extends HistorianBase {
|
|
|
93
94
|
helper: any;
|
|
94
95
|
} | undefined;
|
|
95
96
|
savedFiles: Set<string>;
|
|
96
|
-
constructor(provider: Provider, experienceTracker
|
|
97
|
+
constructor(provider: Provider, experienceTracker: ExperienceTracker, reporter?: Reporter, stateManager?: StateManager, config?: ExplorbotConfig, playwright?: {
|
|
97
98
|
recorder: PlaywrightRecorder;
|
|
98
99
|
helper: any;
|
|
99
100
|
});
|
|
@@ -101,8 +102,8 @@ export declare class Historian extends HistorianBase {
|
|
|
101
102
|
getSavedFiles(): string[];
|
|
102
103
|
savePlanToFile(plan: Plan): string;
|
|
103
104
|
rewriteScenarioInFile(filePath: string, healedSteps: Array<{
|
|
104
|
-
test: string;
|
|
105
105
|
original: string;
|
|
106
106
|
healed: string;
|
|
107
107
|
}>): void;
|
|
108
108
|
}
|
|
109
|
+
export {};
|
package/dist/src/ai/historian.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { ExperienceTracker } from "../experience-tracker.js";
|
|
3
2
|
import { tag } from "../utils/logger.js";
|
|
4
3
|
import { relativeToCwd } from "../utils/next-steps.js";
|
|
5
4
|
import { WithCodeceptJS } from "./historian/codeceptjs.js";
|
|
6
5
|
import { WithExperience } from "./historian/experience.js";
|
|
7
6
|
import { WithPlaywright } from "./historian/playwright.js";
|
|
8
7
|
import { WithScreencast } from "./historian/screencast.js";
|
|
9
|
-
export { isNonReusableCode } from "../utils/step-analyzer.js";
|
|
10
8
|
const HistorianBase = WithScreencast(WithPlaywright(WithCodeceptJS(WithExperience(Object))));
|
|
11
9
|
export class Historian extends HistorianBase {
|
|
12
10
|
constructor(provider, experienceTracker, reporter, stateManager, config, playwright) {
|
|
13
11
|
super();
|
|
14
12
|
this.provider = provider;
|
|
15
|
-
this.experienceTracker = experienceTracker
|
|
13
|
+
this.experienceTracker = experienceTracker;
|
|
16
14
|
this.reporter = reporter;
|
|
17
15
|
this.stateManager = stateManager;
|
|
18
16
|
this.config = config;
|
|
@@ -32,8 +30,6 @@ export class Historian extends HistorianBase {
|
|
|
32
30
|
rewriteScenarioInFile(filePath, healedSteps) {
|
|
33
31
|
let content = readFileSync(filePath, 'utf-8');
|
|
34
32
|
for (const step of healedSteps) {
|
|
35
|
-
if (!content.includes(step.original))
|
|
36
|
-
continue;
|
|
37
33
|
content = content.replace(step.original, step.healed);
|
|
38
34
|
}
|
|
39
35
|
writeFileSync(filePath, content);
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { ActionResult } from '../action-result.js';
|
|
2
2
|
import type Action from '../action.js';
|
|
3
|
-
import { ExperienceTracker } from '../experience-tracker.js';
|
|
3
|
+
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
4
4
|
import Explorer from '../explorer.js';
|
|
5
|
-
import { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
5
|
+
import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
6
6
|
import { HooksRunner } from '../utils/hooks-runner.js';
|
|
7
7
|
import type { Agent } from './agent.js';
|
|
8
|
-
import { ExperienceCompactor } from './experience-compactor.js';
|
|
9
8
|
import type { Provider } from './provider.js';
|
|
10
9
|
declare class Navigator implements Agent {
|
|
11
10
|
emoji: string;
|
|
12
11
|
provider: Provider;
|
|
13
|
-
experienceCompactor: ExperienceCompactor;
|
|
14
12
|
knowledgeTracker: KnowledgeTracker;
|
|
15
13
|
experienceTracker: ExperienceTracker;
|
|
16
14
|
hooksRunner: HooksRunner;
|
|
@@ -18,7 +16,7 @@ declare class Navigator implements Agent {
|
|
|
18
16
|
systemPrompt: string;
|
|
19
17
|
freeSailSystemPrompt: string;
|
|
20
18
|
explorer: Explorer;
|
|
21
|
-
constructor(explorer: Explorer, provider: Provider
|
|
19
|
+
constructor(explorer: Explorer, provider: Provider);
|
|
22
20
|
get verifyAttempts(): number;
|
|
23
21
|
get verifyTimeout(): number;
|
|
24
22
|
getBaseOrigin(): string | null;
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -2,8 +2,6 @@ import { tool } from 'ai';
|
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ActionResult } from '../action-result.js';
|
|
5
|
-
import { ExperienceTracker, renderExperienceToc } from '../experience-tracker.js';
|
|
6
|
-
import { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
7
5
|
import { normalizeUrl } from '../state-manager.js';
|
|
8
6
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
9
7
|
import { HooksRunner } from "../utils/hooks-runner.js";
|
|
@@ -14,12 +12,11 @@ import { extractStatePath } from '../utils/url-matcher.js';
|
|
|
14
12
|
import { Researcher } from "./researcher.js";
|
|
15
13
|
import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
|
|
16
14
|
import { isInteractive } from './task-agent.js';
|
|
17
|
-
import {
|
|
15
|
+
import { createLearnExperienceTool } from "./tools.js";
|
|
18
16
|
const debugLog = createDebug('explorbot:navigator');
|
|
19
17
|
class Navigator {
|
|
20
18
|
emoji = '🧭';
|
|
21
19
|
provider;
|
|
22
|
-
experienceCompactor;
|
|
23
20
|
knowledgeTracker;
|
|
24
21
|
experienceTracker;
|
|
25
22
|
hooksRunner;
|
|
@@ -62,12 +59,11 @@ class Navigator {
|
|
|
62
59
|
</rules>
|
|
63
60
|
`;
|
|
64
61
|
explorer;
|
|
65
|
-
constructor(explorer, provider
|
|
62
|
+
constructor(explorer, provider) {
|
|
66
63
|
this.provider = provider;
|
|
67
64
|
this.explorer = explorer;
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
70
|
-
this.experienceTracker = experienceTracker || new ExperienceTracker();
|
|
65
|
+
this.knowledgeTracker = explorer.getKnowledgeTracker();
|
|
66
|
+
this.experienceTracker = explorer.getStateManager().getExperienceTracker();
|
|
71
67
|
this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
|
|
72
68
|
}
|
|
73
69
|
get verifyAttempts() {
|
|
@@ -171,17 +167,8 @@ class Navigator {
|
|
|
171
167
|
debugLog('Resolution message:', message);
|
|
172
168
|
const action = opts?.action ?? this.explorer.createAction();
|
|
173
169
|
const expectedUrl = opts?.expectedUrl;
|
|
174
|
-
|
|
170
|
+
const knowledge = this.knowledgeTracker.renderRelevantKnowledge(actionResult);
|
|
175
171
|
let experience = '';
|
|
176
|
-
const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
|
|
177
|
-
if (relevantKnowledge.length > 0) {
|
|
178
|
-
const knowledgeContent = relevantKnowledge.map((k) => k.content).join('\n\n');
|
|
179
|
-
knowledge = `
|
|
180
|
-
<hint>
|
|
181
|
-
Here is relevant knowledge for this page:
|
|
182
|
-
${knowledgeContent}
|
|
183
|
-
</hint>`;
|
|
184
|
-
}
|
|
185
172
|
if (!actionResult.isInsideIframe) {
|
|
186
173
|
const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
|
|
187
174
|
if (successful.length > 0) {
|
|
@@ -362,7 +349,6 @@ class Navigator {
|
|
|
362
349
|
if (freshState.getStateHash() !== prevHash) {
|
|
363
350
|
try {
|
|
364
351
|
const diff = await freshState.diff(prevActionResult);
|
|
365
|
-
await diff.calculate();
|
|
366
352
|
ariaChanges = diff.ariaChanged;
|
|
367
353
|
}
|
|
368
354
|
catch (err) {
|
|
@@ -446,21 +432,14 @@ class Navigator {
|
|
|
446
432
|
return resolved;
|
|
447
433
|
}
|
|
448
434
|
buildExperienceTools() {
|
|
435
|
+
if (!this.experienceTracker)
|
|
436
|
+
return undefined;
|
|
449
437
|
const stateManager = this.explorer.getStateManager();
|
|
450
438
|
const getState = () => {
|
|
451
439
|
const s = stateManager.getCurrentState();
|
|
452
440
|
return s ? ActionResult.fromState(s) : null;
|
|
453
441
|
};
|
|
454
|
-
|
|
455
|
-
explorer: this.explorer,
|
|
456
|
-
researcher: null,
|
|
457
|
-
navigator: this,
|
|
458
|
-
experienceTracker: this.experienceTracker,
|
|
459
|
-
getState,
|
|
460
|
-
});
|
|
461
|
-
if (!learnExperience)
|
|
462
|
-
return undefined;
|
|
463
|
-
return { learnExperience };
|
|
442
|
+
return { learnExperience: createLearnExperienceTool({ getExperienceTracker: () => this.experienceTracker, getState }) };
|
|
464
443
|
}
|
|
465
444
|
async freeSail(opts, actionResult) {
|
|
466
445
|
const stateManager = this.explorer.getStateManager();
|
|
@@ -579,24 +558,10 @@ class Navigator {
|
|
|
579
558
|
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
580
559
|
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
581
560
|
}
|
|
582
|
-
|
|
561
|
+
const knowledge = this.knowledgeTracker.renderRelevantKnowledge(actionResult);
|
|
583
562
|
let experience = '';
|
|
584
|
-
const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
|
|
585
|
-
if (relevantKnowledge.length > 0) {
|
|
586
|
-
const knowledgeContent = relevantKnowledge.map((k) => k.content).join('\n\n');
|
|
587
|
-
knowledge = `
|
|
588
|
-
<hint>
|
|
589
|
-
Here is relevant knowledge for this page:
|
|
590
|
-
${knowledgeContent}
|
|
591
|
-
</hint>`;
|
|
592
|
-
}
|
|
593
563
|
if (!actionResult.isInsideIframe) {
|
|
594
|
-
|
|
595
|
-
if (toc.length > 0) {
|
|
596
|
-
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
597
|
-
tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
|
|
598
|
-
experience = renderExperienceToc(toc);
|
|
599
|
-
}
|
|
564
|
+
experience = this.experienceTracker.renderExperienceTocFor(actionResult);
|
|
600
565
|
}
|
|
601
566
|
const priorVerifications = Object.entries(actionResult.verifications ?? {});
|
|
602
567
|
let verificationContext = '';
|
|
@@ -685,7 +650,7 @@ class Navigator {
|
|
|
685
650
|
return;
|
|
686
651
|
}
|
|
687
652
|
await this.explorer.switchToMainFrame();
|
|
688
|
-
const verified = await action.attempt(codeBlock, message
|
|
653
|
+
const verified = await action.attempt(codeBlock, message);
|
|
689
654
|
if (verified) {
|
|
690
655
|
tag('success').log('Verification passed');
|
|
691
656
|
successfulCodes.push(codeBlock);
|
package/dist/src/ai/pilot.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ActionResult } from '../action-result.js';
|
|
2
|
-
import { type ExperienceTracker } from '../experience-tracker.js';
|
|
3
2
|
import type Explorer from '../explorer.js';
|
|
4
3
|
import { type Test } from '../test-plan.js';
|
|
5
4
|
import type { Agent } from './agent.js';
|
|
@@ -16,8 +15,7 @@ export declare class Pilot implements Agent {
|
|
|
16
15
|
researcher: Researcher;
|
|
17
16
|
explorer: Explorer;
|
|
18
17
|
fisherman: Fisherman | null;
|
|
19
|
-
|
|
20
|
-
constructor(provider: Provider, agentTools: any, researcher: Researcher, explorer: Explorer, experienceTracker?: ExperienceTracker);
|
|
18
|
+
constructor(provider: Provider, agentTools: any, researcher: Researcher, explorer: Explorer);
|
|
21
19
|
setFisherman(fisherman: Fisherman): void;
|
|
22
20
|
get stepsToReview(): number;
|
|
23
21
|
reset(): void;
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -3,7 +3,6 @@ import dedent from 'dedent';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ActionResult } from "../action-result.js";
|
|
5
5
|
import { ConfigParser } from "../config.js";
|
|
6
|
-
import { renderExperienceToc } from "../experience-tracker.js";
|
|
7
6
|
import { TestResult } from "../test-plan.js";
|
|
8
7
|
import { collectInteractiveNodes, detectFocusArea, extractFocusedElement } from "../utils/aria.js";
|
|
9
8
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
@@ -22,13 +21,11 @@ export class Pilot {
|
|
|
22
21
|
researcher;
|
|
23
22
|
explorer;
|
|
24
23
|
fisherman = null;
|
|
25
|
-
|
|
26
|
-
constructor(provider, agentTools, researcher, explorer, experienceTracker) {
|
|
24
|
+
constructor(provider, agentTools, researcher, explorer) {
|
|
27
25
|
this.provider = provider;
|
|
28
26
|
this.agentTools = agentTools;
|
|
29
27
|
this.researcher = researcher;
|
|
30
28
|
this.explorer = explorer;
|
|
31
|
-
this.experienceTracker = experienceTracker || null;
|
|
32
29
|
}
|
|
33
30
|
setFisherman(fisherman) {
|
|
34
31
|
this.fisherman = fisherman;
|
|
@@ -90,7 +87,7 @@ export class Pilot {
|
|
|
90
87
|
requestVerification: z
|
|
91
88
|
.string()
|
|
92
89
|
.nullable()
|
|
93
|
-
.describe('REQUIRED whenever decision is "pass" — a one-sentence natural-language claim about the current page that, if true, proves the scenario goal (e.g., "New
|
|
90
|
+
.describe('REQUIRED whenever decision is "pass" — a one-sentence natural-language claim about the current page that, if true, proves the scenario goal (e.g., "New item \\"Foo\\" is visible in the items list"). NOT code: do not write I.*, expect(), .then(), grabTitle, or any JavaScript. Navigator translates the claim into CodeceptJS assertions and runs them; passing assertions are saved to the generated test file. Also use when evidence is insufficient before deciding pass/fail. Leave null for "continue", "fail", or "skipped".'),
|
|
94
91
|
});
|
|
95
92
|
const userContent = dedent `
|
|
96
93
|
Tester wants to ${type} the test.
|
|
@@ -124,7 +121,7 @@ export class Pilot {
|
|
|
124
121
|
- Mixed evidence + final state shows success → pass. Mixed + final state unclear → continue with guidance.
|
|
125
122
|
|
|
126
123
|
When deciding "pass", you MUST also set requestVerification to a one-sentence natural-language
|
|
127
|
-
claim about the current page (e.g., "New
|
|
124
|
+
claim about the current page (e.g., "New item Foo is visible in the items list"). NOT
|
|
128
125
|
code — do not write I.*, expect(), .then(), or any JavaScript. Choose the strongest single
|
|
129
126
|
piece of evidence (a unique element/text that exists ONLY because the scenario succeeded).
|
|
130
127
|
Navigator translates the claim into CodeceptJS assertions; without it the generated test has
|
|
@@ -379,10 +376,6 @@ export class Pilot {
|
|
|
379
376
|
|
|
380
377
|
FIRST: Decide if precondition() is needed.
|
|
381
378
|
|
|
382
|
-
${capabilityGroundingRule}
|
|
383
|
-
|
|
384
|
-
${dataProtectionRules}
|
|
385
|
-
|
|
386
379
|
Call precondition() WHEN:
|
|
387
380
|
- The scenario edits/deletes/modifies an item, and you want a DISPOSABLE item to act on safely
|
|
388
381
|
- The scenario needs specific data clearly NOT on the current page (e.g., items with specific statuses for filtering)
|
|
@@ -525,14 +518,10 @@ export class Pilot {
|
|
|
525
518
|
`;
|
|
526
519
|
}
|
|
527
520
|
getExperienceToc() {
|
|
528
|
-
if (!this.experienceTracker)
|
|
529
|
-
return '';
|
|
530
521
|
const state = this.explorer.getStateManager().getCurrentState();
|
|
531
522
|
if (!state)
|
|
532
523
|
return '';
|
|
533
|
-
|
|
534
|
-
const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
|
|
535
|
-
return renderExperienceToc(toc);
|
|
524
|
+
return this.explorer.getStateManager().getExperienceTracker().renderExperienceTocFor(ActionResult.fromState(state));
|
|
536
525
|
}
|
|
537
526
|
pickPlanningTools() {
|
|
538
527
|
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser } = this.agentTools ?? {};
|
|
@@ -957,8 +946,8 @@ export class Pilot {
|
|
|
957
946
|
|
|
958
947
|
${dataProtectionRules}
|
|
959
948
|
|
|
960
|
-
Describe WHAT to create, not what exists. RIGHT: precondition("1
|
|
961
|
-
precondition("1
|
|
949
|
+
Describe WHAT to create, not what exists. RIGHT: precondition("1 post"). WRONG:
|
|
950
|
+
precondition("1 post named Updated Post with existing comments"). Keep descriptions short.
|
|
962
951
|
|
|
963
952
|
Response format:
|
|
964
953
|
PROGRESS: <1 sentence assessment>
|
package/dist/src/ai/planner.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionResult } from '../action-result.js';
|
|
2
|
-
import { ExperienceTracker } from '../experience-tracker.js';
|
|
2
|
+
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
3
3
|
import type Explorer from '../explorer.js';
|
|
4
4
|
import type { StateManager } from '../state-manager.js';
|
|
5
5
|
import { Suite } from '../suite.js';
|
|
@@ -8,7 +8,7 @@ import type { Agent } from './agent.js';
|
|
|
8
8
|
import { Conversation } from './conversation.js';
|
|
9
9
|
import type { Fisherman } from './fisherman.js';
|
|
10
10
|
import type { Provider } from './provider.js';
|
|
11
|
-
import { Researcher } from './researcher.js';
|
|
11
|
+
import { type Researcher } from './researcher.js';
|
|
12
12
|
declare const PlannerBase: {
|
|
13
13
|
new (...args: any[]): {
|
|
14
14
|
currentPlan: Plan | null;
|
|
@@ -58,7 +58,7 @@ export declare class Planner extends PlannerBase implements Agent {
|
|
|
58
58
|
lastSuite: Suite | null;
|
|
59
59
|
researcher: Researcher;
|
|
60
60
|
fisherman: Fisherman | null;
|
|
61
|
-
constructor(explorer: Explorer, provider: Provider);
|
|
61
|
+
constructor(explorer: Explorer, provider: Provider, researcher: Researcher);
|
|
62
62
|
setFisherman(fisherman: Fisherman): void;
|
|
63
63
|
get sectionOrder(): string[];
|
|
64
64
|
getDefaultStartUrl(state: {
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -3,7 +3,6 @@ import { z } from 'zod';
|
|
|
3
3
|
import { ActionResult } from "../action-result.js";
|
|
4
4
|
import { setActivity } from "../activity.js";
|
|
5
5
|
import { ConfigParser } from "../config.js";
|
|
6
|
-
import { ExperienceTracker } from "../experience-tracker.js";
|
|
7
6
|
import { Observability } from "../observability.js";
|
|
8
7
|
import { Stats } from "../stats.js";
|
|
9
8
|
import { Suite } from "../suite.js";
|
|
@@ -16,7 +15,7 @@ import { Conversation } from "./conversation.js";
|
|
|
16
15
|
import { WithSessionDedup } from "./planner/session-dedup.js";
|
|
17
16
|
import { getActiveStyle } from "./planner/styles.js";
|
|
18
17
|
import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from "./planner/subpages.js";
|
|
19
|
-
import { POSSIBLE_SECTIONS
|
|
18
|
+
import { POSSIBLE_SECTIONS } from "./researcher.js";
|
|
20
19
|
import { findSimilarStateHash } from "./researcher/cache.js";
|
|
21
20
|
import { hasFocusedSection } from "./researcher/focus.js";
|
|
22
21
|
import { capabilityGroundingRule, dataProtectionRules, fileUploadRule } from "./rules.js";
|
|
@@ -50,13 +49,13 @@ export class Planner extends PlannerBase {
|
|
|
50
49
|
lastSuite = null;
|
|
51
50
|
researcher;
|
|
52
51
|
fisherman = null;
|
|
53
|
-
constructor(explorer, provider) {
|
|
52
|
+
constructor(explorer, provider, researcher) {
|
|
54
53
|
super();
|
|
55
54
|
this.explorer = explorer;
|
|
56
55
|
this.provider = provider;
|
|
57
|
-
this.researcher =
|
|
56
|
+
this.researcher = researcher;
|
|
58
57
|
this.stateManager = explorer.getStateManager();
|
|
59
|
-
this.experienceTracker =
|
|
58
|
+
this.experienceTracker = explorer.getStateManager().getExperienceTracker();
|
|
60
59
|
}
|
|
61
60
|
setFisherman(fisherman) {
|
|
62
61
|
this.fisherman = fisherman;
|
|
@@ -81,7 +80,7 @@ export class Planner extends PlannerBase {
|
|
|
81
80
|
You are ISTQB certified senior manual QA planning exploratory testing session of a web application.
|
|
82
81
|
Each test scenario must complete a meaningful user workflow — not just open a UI element and verify it exists.
|
|
83
82
|
Bad: "Open the Help panel" — just opens something.
|
|
84
|
-
Good: "Create a new
|
|
83
|
+
Good: "Create a new post and verify it appears in the list" — completes a business action.
|
|
85
84
|
</role>
|
|
86
85
|
<task>
|
|
87
86
|
List possible testing scenarios for the web page.
|
|
@@ -149,6 +148,7 @@ export class Planner extends PlannerBase {
|
|
|
149
148
|
const tags = ['planner'];
|
|
150
149
|
if (style)
|
|
151
150
|
tags.push(style);
|
|
151
|
+
const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
|
|
152
152
|
const result = await Observability.run(`planner: ${state.url}`, { tags, sessionId: state.url }, async () => {
|
|
153
153
|
const actionResult = ActionResult.fromState(state);
|
|
154
154
|
const conversation = await this.buildConversation(actionResult, style, parentPlan, feature);
|
|
@@ -182,7 +182,6 @@ export class Planner extends PlannerBase {
|
|
|
182
182
|
if (parentPlan)
|
|
183
183
|
this.currentPlan.parentPlan = parentPlan;
|
|
184
184
|
const allPreviousScenarios = this.getPreviousSessionScenarios();
|
|
185
|
-
const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
|
|
186
185
|
for (const s of existingTestScenarios)
|
|
187
186
|
allPreviousScenarios.add(s);
|
|
188
187
|
for (const t of tests) {
|
|
@@ -232,6 +231,7 @@ export class Planner extends PlannerBase {
|
|
|
232
231
|
return added;
|
|
233
232
|
}
|
|
234
233
|
getExistingTestFileScenarios(currentUrl) {
|
|
234
|
+
this.lastSuite = null;
|
|
235
235
|
if (!currentUrl)
|
|
236
236
|
return new Set();
|
|
237
237
|
try {
|
|
@@ -244,27 +244,37 @@ export class Planner extends PlannerBase {
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
cleanExperienceFlows(text) {
|
|
247
|
-
const seenTitles = new Set();
|
|
248
247
|
let result = text;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
.query(
|
|
253
|
-
.
|
|
254
|
-
.
|
|
255
|
-
.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
248
|
+
const seenTitles = new Set();
|
|
249
|
+
for (const selector of ['section2', 'section3']) {
|
|
250
|
+
result = mdq(result)
|
|
251
|
+
.query(selector)
|
|
252
|
+
.replaceEach((section) => {
|
|
253
|
+
const heading = section.query('heading').text().trim();
|
|
254
|
+
const withoutHeadings = mdq(section.text()).query('heading').replace('');
|
|
255
|
+
const body = mdq(withoutHeadings).query('hr').replace('').trim();
|
|
256
|
+
if (body && !seenTitles.has(heading)) {
|
|
257
|
+
seenTitles.add(heading);
|
|
258
|
+
return section.text();
|
|
259
|
+
}
|
|
260
|
+
return '';
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const trimmedTitles = new Set();
|
|
264
|
+
for (const selector of ['section2', 'section3']) {
|
|
265
|
+
result = mdq(result)
|
|
266
|
+
.query(selector)
|
|
267
|
+
.replaceEach((section) => {
|
|
268
|
+
const heading = section.query('heading').text().trim();
|
|
269
|
+
if (trimmedTitles.has(heading))
|
|
270
|
+
return section.text();
|
|
271
|
+
const count = section.query('blockquote').count();
|
|
272
|
+
if (count <= 10)
|
|
273
|
+
return section.text();
|
|
274
|
+
const kept = mdq(section.text()).query('blockquote[10:]').replace('');
|
|
275
|
+
trimmedTitles.add(heading);
|
|
276
|
+
return `${kept.trimEnd()}\n> ... and ${count - 10} more discoveries\n`;
|
|
277
|
+
});
|
|
268
278
|
}
|
|
269
279
|
return result.trim() || null;
|
|
270
280
|
}
|
|
@@ -298,6 +308,7 @@ export class Planner extends PlannerBase {
|
|
|
298
308
|
If there are subpages (pages with same URL path) plan testing of those subpages as well
|
|
299
309
|
If you plan to test CRUD operations, plan them in correct order: create, read, update.
|
|
300
310
|
Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
|
|
311
|
+
When validation placement or wording was not observed, require feedback associated with the invalid input without inventing a specific location or message.
|
|
301
312
|
If exact wording is unknown, describe the expected result generically, for example "an authentication error is shown" or "the user stays on the login page" instead of guessing the literal text.
|
|
302
313
|
If exact redirect destination is unknown, describe the destination by visible page identity, for example "the dashboard page opens" or "the current workspace home page opens" instead of inventing a URL slug.
|
|
303
314
|
Only propose scenarios whose prerequisites are evident from page research, visited pages, or API data preparation context.
|
|
@@ -337,17 +348,18 @@ export class Planner extends PlannerBase {
|
|
|
337
348
|
deep: true,
|
|
338
349
|
});
|
|
339
350
|
let plannerResearch = mdq(research).query('code').replace('');
|
|
340
|
-
|
|
341
|
-
|
|
351
|
+
plannerResearch = mdq(plannerResearch)
|
|
352
|
+
.query('table')
|
|
353
|
+
.replaceEach((table) => {
|
|
342
354
|
const rows = table.toJson();
|
|
343
355
|
if (rows.length === 0 || !rows[0].Element)
|
|
344
|
-
|
|
356
|
+
return table.text();
|
|
345
357
|
const elementWithType = rows.map((r) => ({
|
|
346
358
|
Element: r.Element,
|
|
347
359
|
Type: r.Type || '',
|
|
348
360
|
}));
|
|
349
|
-
|
|
350
|
-
}
|
|
361
|
+
return jsonToTable(elementWithType, ['Element', 'Type']);
|
|
362
|
+
});
|
|
351
363
|
const hasFocusedOverlay = hasFocusedSection(plannerResearch);
|
|
352
364
|
const focusNote = hasFocusedOverlay ? "IMPORTANT: One section is marked as **Focused** — this is the user's current focus area. Concentrate testing on the Focused section FIRST — test all interactions inside it before planning tests for the rest of the page." : '';
|
|
353
365
|
const featureFilter = feature ? `FOCUS FILTER: Only propose scenarios using elements relevant to "${feature}". Ignore all other elements.` : '';
|
|
@@ -525,17 +537,17 @@ export class Planner extends PlannerBase {
|
|
|
525
537
|
|
|
526
538
|
STEPS - actionable commands:
|
|
527
539
|
- Each step should be a specific action to perform
|
|
528
|
-
- Good: "Click Create
|
|
529
|
-
- Good: "Click Star icon on '
|
|
540
|
+
- Good: "Click Create Post button", "Enter 'My New Post' as post title", "Click Save button"
|
|
541
|
+
- Good: "Click Star icon on 'Quarterly Report' item"
|
|
530
542
|
- Bad: "Open the dropdown menu" (opening is not a goal, it's a means)
|
|
531
543
|
- Bad: "Verify modal appears" (verification belongs in expected outcomes, not steps)
|
|
532
544
|
- Steps should form a complete workflow, not stop at opening a UI element
|
|
533
545
|
- NEVER split a workflow across two tests. One test = one complete action + its verification
|
|
534
546
|
|
|
535
547
|
EXPECTED OUTCOMES - verifiable results:
|
|
536
|
-
- Good: "New
|
|
537
|
-
- Good: "
|
|
538
|
-
- Good: "Success message '
|
|
548
|
+
- Good: "New post 'My New Post' appears in the posts list"
|
|
549
|
+
- Good: "Item appears under Starred filter tab"
|
|
550
|
+
- Good: "Success message 'Post created' is displayed"
|
|
539
551
|
- Good when wording is unknown: "An authentication error is displayed"
|
|
540
552
|
- Good when route is unknown: "The workspace home page is displayed"
|
|
541
553
|
- Bad: "Modal is displayed" (just verifying existence, no business value)
|