explorbot 0.5.9 → 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/README.md +1 -1
- package/bin/explorbot-cli.ts +495 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/api-tester/src/commands/init-command.ts +2 -0
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -1
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/dist/bin/explorbot-cli.js +456 -444
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -1
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/boat/api-tester/src/commands/init-command.js +2 -0
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -1
- package/dist/boat/prima/bin/prima-cli.js +2 -1
- 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 +8 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/options/auth-option.d.ts +6 -0
- package/dist/src/commands/options/auth-option.js +14 -0
- package/dist/src/commands/options/index.d.ts +2 -0
- package/dist/src/commands/options/index.js +2 -0
- 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 +3 -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 +18 -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/docs/workflow/agentic-usage.md +7 -0
- package/docs/workflow/knowledge.md +6 -0
- 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 +10 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/options/auth-option.ts +15 -0
- package/src/commands/options/index.ts +2 -0
- 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 +5 -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/src/utils/logger.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { type Span, context, trace } from '@opentelemetry/api';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import debug from 'debug';
|
|
6
6
|
import dedent from 'dedent';
|
|
7
|
-
import { marked } from 'marked';
|
|
8
7
|
import stripAnsi from 'strip-ansi';
|
|
8
|
+
import wrapAnsi from 'wrap-ansi';
|
|
9
9
|
import { ConfigParser } from '../config.js';
|
|
10
10
|
import { Observability } from '../observability.ts';
|
|
11
11
|
import { RecentStepFilter } from './log-filters.ts';
|
|
@@ -81,6 +81,11 @@ class ConsoleDestination implements LogDestination {
|
|
|
81
81
|
private forceEnabled = false;
|
|
82
82
|
private quiet = false;
|
|
83
83
|
private recentSteps = new RecentStepFilter();
|
|
84
|
+
private width: number;
|
|
85
|
+
|
|
86
|
+
constructor() {
|
|
87
|
+
this.width = process.stdout.columns || 80;
|
|
88
|
+
}
|
|
84
89
|
|
|
85
90
|
isEnabled(): boolean {
|
|
86
91
|
if (this.quiet) return false;
|
|
@@ -113,8 +118,8 @@ class ConsoleDestination implements LogDestination {
|
|
|
113
118
|
if (entry.type === 'multiline' || entry.type === 'details') {
|
|
114
119
|
const cleaned = stripAnsi(dedent(entry.content));
|
|
115
120
|
const parsed = parseMarkdownToTerminal(cleaned);
|
|
116
|
-
|
|
117
|
-
content = `${chalk.
|
|
121
|
+
|
|
122
|
+
content = `${chalk.dim('\n ┌'.padEnd(this.width, '─'))}\n${this.prefixLines(parsed, ' │ ', chalk.dim, false)}\n`;
|
|
118
123
|
} else if (entry.type === 'success') {
|
|
119
124
|
content = `${chalk.green(content)}\n`;
|
|
120
125
|
} else if (entry.type === 'error') {
|
|
@@ -122,14 +127,32 @@ class ConsoleDestination implements LogDestination {
|
|
|
122
127
|
} else if (entry.type === 'warning') {
|
|
123
128
|
content = chalk.yellow(content);
|
|
124
129
|
} else if (entry.type === 'step') {
|
|
125
|
-
content = chalk.gray
|
|
130
|
+
content = this.prefixLines(content, ' ', chalk.gray);
|
|
126
131
|
} else if (entry.type === 'operation') {
|
|
127
|
-
content =
|
|
132
|
+
content = this.prefixLines(content, ' · ', chalk.gray);
|
|
128
133
|
} else if (entry.type === 'substep') {
|
|
129
|
-
content =
|
|
134
|
+
content = this.prefixLines(content, ' > ', chalk.gray);
|
|
130
135
|
}
|
|
131
136
|
console.log(content);
|
|
132
137
|
}
|
|
138
|
+
|
|
139
|
+
private prefixLines(content: string, prefix: string, style: (s: string) => string, strip = true): string {
|
|
140
|
+
const prefixWidth = stripAnsi(prefix).length;
|
|
141
|
+
const available = Math.max(20, this.width - prefixWidth);
|
|
142
|
+
const lines = content
|
|
143
|
+
.split('\n')
|
|
144
|
+
.filter((line) => stripAnsi(line).trim() !== '')
|
|
145
|
+
.flatMap((line) => {
|
|
146
|
+
const text = strip ? stripAnsi(line) : line;
|
|
147
|
+
const wrapped = wrapAnsi(text, available, { hard: true });
|
|
148
|
+
return wrapped.split('\n').map((part) => style(`${prefix}${part}`));
|
|
149
|
+
});
|
|
150
|
+
if (lines.length > 10) {
|
|
151
|
+
const remaining = lines.length - 10;
|
|
152
|
+
return [...lines.slice(0, 10), chalk.gray(` │ --- +${remaining} lines ---`)].join('\n');
|
|
153
|
+
}
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
class DebugDestination implements LogDestination {
|
|
@@ -160,16 +183,11 @@ class DebugDestination implements LogDestination {
|
|
|
160
183
|
class FileDestination implements LogDestination {
|
|
161
184
|
private initialized = false;
|
|
162
185
|
private logFilePath: string | null = null;
|
|
163
|
-
private verboseMode = true;
|
|
164
186
|
|
|
165
187
|
isEnabled(): boolean {
|
|
166
188
|
return true;
|
|
167
189
|
}
|
|
168
190
|
|
|
169
|
-
setVerboseMode(enabled: boolean): void {
|
|
170
|
-
this.verboseMode = enabled;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
191
|
write(entry: TaggedLogEntry): void {
|
|
174
192
|
if (entry.type === 'html') return;
|
|
175
193
|
if (entry.type === 'multiline') return;
|
|
@@ -404,7 +422,7 @@ class Logger {
|
|
|
404
422
|
private truncateTagContent(content: string, tagName: string): string {
|
|
405
423
|
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
406
424
|
const tagRegex = new RegExp(`<${escapedTag}>([\\s\\S]*?)<\\/${escapedTag}>`, 'g');
|
|
407
|
-
return content.replace(tagRegex, (
|
|
425
|
+
return content.replace(tagRegex, (_, innerContent) => {
|
|
408
426
|
const trimmed = innerContent.trim();
|
|
409
427
|
const totalLength = trimmed.length;
|
|
410
428
|
if (totalLength <= 100) {
|
|
@@ -24,10 +24,9 @@ if (renderer) {
|
|
|
24
24
|
const originalListitem = renderer.listitem;
|
|
25
25
|
renderer.listitem = function (text: any) {
|
|
26
26
|
if (typeof text === 'object') {
|
|
27
|
-
const item = text;
|
|
28
27
|
const parser = this.parser;
|
|
29
28
|
const originalParse = parser.parse.bind(parser);
|
|
30
|
-
parser.parse = (tokens: any
|
|
29
|
+
parser.parse = (tokens: any) => parser.parseInline(tokens);
|
|
31
30
|
const result = originalListitem.call(this, text);
|
|
32
31
|
parser.parse = originalParse;
|
|
33
32
|
return result;
|
package/src/utils/next-steps.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import figures from 'figures';
|
|
2
4
|
import { tag } from './logger.js';
|
|
3
5
|
|
|
4
6
|
export interface NextStepCommand {
|
|
@@ -20,27 +22,27 @@ export function relativeToCwd(absPath: string): string {
|
|
|
20
22
|
export function printNextSteps(sections: NextStepSection[]): void {
|
|
21
23
|
if (sections.length === 0) return;
|
|
22
24
|
|
|
23
|
-
const
|
|
25
|
+
const lines: string[] = [''];
|
|
26
|
+
let isDefault = true;
|
|
24
27
|
for (const section of sections) {
|
|
25
|
-
const lines: string[] = [];
|
|
26
28
|
const headerPath = section.path ? relativeToCwd(section.path) : '';
|
|
27
|
-
lines.push(headerPath ? `${section.label}
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
const padded = `${cmd.label}:`.padEnd(maxLabel + 2);
|
|
39
|
-
lines.push(` ${padded} ${cmd.command}`);
|
|
29
|
+
lines.push(headerPath ? `${chalk.bold(section.label)} ${chalk.gray(headerPath)}` : `${chalk.bold(section.label)}`);
|
|
30
|
+
|
|
31
|
+
for (const cmd of section.commands || []) {
|
|
32
|
+
if (cmd.label) lines.push(BLANK);
|
|
33
|
+
if (isDefault) {
|
|
34
|
+
if (cmd.label) lines.push(chalk.bold.yellow(`${figures.pointer} ${cmd.label}`));
|
|
35
|
+
lines.push(chalk.bold(` ${cmd.command}`));
|
|
36
|
+
isDefault = false;
|
|
37
|
+
continue;
|
|
40
38
|
}
|
|
39
|
+
if (cmd.label) lines.push(chalk.dim(` ${cmd.label}`));
|
|
40
|
+
lines.push(` ${cmd.command}`);
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
lines.push(BLANK);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
tag('
|
|
45
|
+
tag('info').log(lines.join('\n'));
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
const BLANK = ' ';
|
|
@@ -1,11 +1,33 @@
|
|
|
1
|
-
export async function waitForPageReadiness(page: any, options: PageReadinessOptions = {}): Promise<
|
|
2
|
-
if (!page) return;
|
|
1
|
+
export async function waitForPageReadiness(page: any, options: PageReadinessOptions = {}): Promise<boolean> {
|
|
2
|
+
if (!page) return false;
|
|
3
3
|
|
|
4
4
|
const timeout = options.timeout ?? 6000;
|
|
5
5
|
await page.waitForLoadState?.('domcontentloaded', { timeout })?.catch(() => {});
|
|
6
6
|
|
|
7
7
|
await Promise.race([waitForNetworkIdle(page, timeout), waitForDomQuiet(page, timeout), waitForVisibleSpinnersHidden(page, options.spinnerSelectors || [], timeout), sleep(timeout)]).catch(() => {});
|
|
8
|
-
|
|
8
|
+
return waitForPageBodyContent(page, timeout);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function waitForDomChange(page: any, timeout: number): Promise<boolean> {
|
|
12
|
+
if (!page?.evaluate) return Promise.resolve(false);
|
|
13
|
+
|
|
14
|
+
return page
|
|
15
|
+
.evaluate(
|
|
16
|
+
(ms: number) =>
|
|
17
|
+
new Promise<boolean>((resolve) => {
|
|
18
|
+
const observer = new MutationObserver(() => {
|
|
19
|
+
observer.disconnect();
|
|
20
|
+
resolve(true);
|
|
21
|
+
});
|
|
22
|
+
observer.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
observer.disconnect();
|
|
25
|
+
resolve(false);
|
|
26
|
+
}, ms);
|
|
27
|
+
}),
|
|
28
|
+
timeout
|
|
29
|
+
)
|
|
30
|
+
.catch(() => true);
|
|
9
31
|
}
|
|
10
32
|
|
|
11
33
|
const DOM_QUIET_MS = 350;
|
|
@@ -56,20 +78,24 @@ async function waitForVisibleSpinnersHidden(page: any, selectors: string[], time
|
|
|
56
78
|
return Promise.all(visibleSpinners.map((locator) => locator.waitFor({ state: 'hidden', timeout }).catch(() => {}))).then(() => {});
|
|
57
79
|
}
|
|
58
80
|
|
|
59
|
-
function waitForPageBodyContent(page: any, timeout: number): Promise<
|
|
60
|
-
if (!page?.waitForFunction) return Promise.resolve();
|
|
81
|
+
function waitForPageBodyContent(page: any, timeout: number): Promise<boolean> {
|
|
82
|
+
if (!page?.waitForFunction) return Promise.resolve(true);
|
|
61
83
|
|
|
62
84
|
return page
|
|
63
85
|
.waitForFunction(
|
|
64
86
|
() => {
|
|
65
87
|
const body = document.body;
|
|
66
88
|
if (!body) return false;
|
|
67
|
-
|
|
89
|
+
if (body.innerText.trim().length > 0) return true;
|
|
90
|
+
return !!body.querySelector('input, button, select, textarea, img, svg, canvas, iframe, video');
|
|
68
91
|
},
|
|
69
92
|
undefined,
|
|
70
93
|
{ timeout }
|
|
71
94
|
)
|
|
72
|
-
.
|
|
95
|
+
.then(
|
|
96
|
+
() => true,
|
|
97
|
+
() => false
|
|
98
|
+
);
|
|
73
99
|
}
|
|
74
100
|
|
|
75
101
|
export function sleep(ms: number): Promise<void> {
|
|
@@ -3,6 +3,7 @@ import { type Note, Plan, Test } from '../test-plan.ts';
|
|
|
3
3
|
import { mdq } from './markdown-query.ts';
|
|
4
4
|
|
|
5
5
|
const NOISE_PREFIXES = ['Test started', 'Finish requested:', 'Session name:'];
|
|
6
|
+
const CURRENT_PAGE = 'Current page';
|
|
6
7
|
|
|
7
8
|
function isNoiseNote(message: string): boolean {
|
|
8
9
|
return NOISE_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
@@ -118,7 +119,10 @@ export function parsePlanFromMarkdown(filePath: string): Plan {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
export function parsePlansFromMarkdown(filePath: string): Plan[] {
|
|
121
|
-
|
|
122
|
+
return parsePlansFromMarkdownText(readFileSync(filePath, 'utf-8'));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parsePlansFromMarkdownText(content: string): Plan[] {
|
|
122
126
|
const lines = content.split('\n');
|
|
123
127
|
|
|
124
128
|
const plans: Plan[] = [];
|
|
@@ -193,7 +197,7 @@ export function parsePlansFromMarkdown(filePath: string): Plan[] {
|
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
if (currentTest && inRequirements && line && !line.startsWith('##')) {
|
|
196
|
-
currentTest.startUrl = line;
|
|
200
|
+
if (line !== CURRENT_PAGE) currentTest.startUrl = line;
|
|
197
201
|
continue;
|
|
198
202
|
}
|
|
199
203
|
|
|
@@ -217,7 +221,7 @@ export function parsePlansFromMarkdown(filePath: string): Plan[] {
|
|
|
217
221
|
if (suiteStart === -1) continue;
|
|
218
222
|
const nextSuite = content.indexOf('<!-- suite -->', suiteStart + 1);
|
|
219
223
|
const suiteContent = nextSuite === -1 ? content.slice(suiteStart) : content.slice(suiteStart, nextSuite);
|
|
220
|
-
const firstItem = mdq(suiteContent).query('section[0] item[0]').
|
|
224
|
+
const firstItem = mdq(suiteContent).query('section[0] item[0]').nodes()[0]?.text.trim() || '';
|
|
221
225
|
const urlMatch = firstItem.match(/^URL:\s*(.+)/);
|
|
222
226
|
if (urlMatch) {
|
|
223
227
|
plan.url = urlMatch[1].replace(/\*\*|`|\*|_|~~?/g, '').trim();
|
|
@@ -230,6 +234,34 @@ export function parsePlansFromMarkdown(filePath: string): Plan[] {
|
|
|
230
234
|
return plans;
|
|
231
235
|
}
|
|
232
236
|
|
|
237
|
+
export function findPlanMarkdownProblems(markdown: string): string[] {
|
|
238
|
+
const doc = mdq(markdown);
|
|
239
|
+
const problems: string[] = [];
|
|
240
|
+
const headings = doc.query('h1').nodes();
|
|
241
|
+
if (!doc.query('comment(/^suite/)').exists() || headings.length === 0) {
|
|
242
|
+
problems.push('Missing <!-- suite --> comment followed by the plan name heading');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const scenarioCount = Math.max(headings.length - 1, 0);
|
|
246
|
+
const testComments = doc.query('comment(/^test/)').count();
|
|
247
|
+
if (testComments !== scenarioCount) {
|
|
248
|
+
problems.push(`Found ${scenarioCount} scenario headings but ${testComments} <!-- test --> comments`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const section of doc.query('section1').each().slice(1)) {
|
|
252
|
+
const title = section.query('heading').nodes()[0]?.text;
|
|
253
|
+
if (!section.query('section2("Steps") item').exists()) problems.push(`Scenario "${title}" has no ## Steps items`);
|
|
254
|
+
if (!section.query('section2("Expected") item').exists()) problems.push(`Scenario "${title}" has no ## Expected items`);
|
|
255
|
+
}
|
|
256
|
+
return problems;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function readPlanMarkdownComment(markdown: string, key: string): string | null {
|
|
260
|
+
const body = mdq(markdown).query(`comment(/^${key}:/)`).nodes()[0]?.text;
|
|
261
|
+
if (!body) return null;
|
|
262
|
+
return body.slice(key.length + 1).trim() || null;
|
|
263
|
+
}
|
|
264
|
+
|
|
233
265
|
function formatPlanSuite(plan: Plan): string {
|
|
234
266
|
let content = `<!-- suite -->\n# ${plan.title}\n\n`;
|
|
235
267
|
|
|
@@ -245,7 +277,7 @@ function formatPlanSuite(plan: Plan): string {
|
|
|
245
277
|
content += `<!-- test\npriority: ${test.priority}\n-->\n`;
|
|
246
278
|
content += `# ${test.scenario}\n\n`;
|
|
247
279
|
content += '## Requirements\n';
|
|
248
|
-
content += `${test.startUrl ||
|
|
280
|
+
content += `${test.startUrl || CURRENT_PAGE}\n\n`;
|
|
249
281
|
|
|
250
282
|
if (test.plannedSteps.length > 0) {
|
|
251
283
|
content += '## Steps\n';
|
package/src/utils/url-matcher.ts
CHANGED
|
@@ -44,6 +44,12 @@ export function isSamePageFamily(urlA: string, urlB: string): boolean {
|
|
|
44
44
|
return true;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export function isSamePageIgnoringQuery(urlA: string, urlB: string): boolean {
|
|
48
|
+
const a = new URL(urlA, 'http://localhost');
|
|
49
|
+
const b = new URL(urlB, 'http://localhost');
|
|
50
|
+
return a.pathname === b.pathname && a.hash === b.hash;
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
export function generalizeSegment(segment: string): string {
|
|
48
54
|
if (/^\d+$/.test(segment)) return '\\d+';
|
|
49
55
|
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment)) return '[a-f0-9-]+';
|