@matware/e2e-runner 1.1.0 → 1.2.1
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/.claude-plugin/plugin.json +9 -0
- package/.mcp.json +9 -0
- package/README.md +505 -279
- package/agents/test-analyzer.md +81 -0
- package/agents/test-creator.md +102 -0
- package/agents/test-improver.md +140 -0
- package/bin/cli.js +275 -7
- package/commands/create-test.md +50 -0
- package/commands/run.md +49 -0
- package/commands/verify-issue.md +63 -0
- package/package.json +11 -3
- package/skills/e2e-testing/SKILL.md +166 -0
- package/skills/e2e-testing/references/action-types.md +100 -0
- package/skills/e2e-testing/references/test-json-format.md +159 -0
- package/skills/e2e-testing/references/troubleshooting.md +182 -0
- package/src/actions.js +280 -17
- package/src/ai-generate.js +122 -11
- package/src/config.js +58 -0
- package/src/dashboard.js +173 -10
- package/src/db.js +232 -17
- package/src/index.js +9 -3
- package/src/learner-markdown.js +177 -0
- package/src/learner-neo4j.js +255 -0
- package/src/learner-sqlite.js +354 -0
- package/src/learner.js +413 -0
- package/src/mcp-tools.js +575 -16
- package/src/module-resolver.js +273 -0
- package/src/narrate.js +225 -0
- package/src/neo4j-pool.js +124 -0
- package/src/reporter.js +47 -2
- package/src/runner.js +180 -40
- package/src/verify.js +19 -5
- package/templates/build-dashboard.js +28 -0
- package/templates/dashboard/app.js +1152 -0
- package/templates/dashboard/styles.css +413 -0
- package/templates/dashboard/template.html +201 -0
- package/templates/dashboard.html +1091 -268
- package/templates/docker-compose-neo4j.yml +19 -0
- package/templates/e2e.config.js +3 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learnings markdown generator.
|
|
3
|
+
*
|
|
4
|
+
* Generates {cwd}/e2e/learnings.md after each run, reading from SQLite.
|
|
5
|
+
* The file is designed to be portable, versionable in git, and human-readable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import {
|
|
11
|
+
getLearningsSummary,
|
|
12
|
+
getFlakySummary,
|
|
13
|
+
getSelectorStability,
|
|
14
|
+
getPageHealth,
|
|
15
|
+
getApiHealth,
|
|
16
|
+
getErrorPatterns,
|
|
17
|
+
getTestTrends,
|
|
18
|
+
} from './learner-sqlite.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generates the learnings.md file for a project.
|
|
22
|
+
* Reads from SQLite and writes to {cwd}/e2e/learnings.md.
|
|
23
|
+
*/
|
|
24
|
+
export function generateLearningsMarkdown(projectId, config) {
|
|
25
|
+
const days = config?.learningsDays || 30;
|
|
26
|
+
const summary = getLearningsSummary(projectId);
|
|
27
|
+
const flaky = getFlakySummary(projectId, days);
|
|
28
|
+
const selectors = getSelectorStability(projectId, days);
|
|
29
|
+
const pages = getPageHealth(projectId, days);
|
|
30
|
+
const apis = getApiHealth(projectId, days);
|
|
31
|
+
const errors = getErrorPatterns(projectId);
|
|
32
|
+
const trendsResult = getTestTrends(projectId, 7);
|
|
33
|
+
const trends = trendsResult.data || trendsResult;
|
|
34
|
+
const trendsGranularity = trendsResult.granularity || 'daily';
|
|
35
|
+
|
|
36
|
+
const lines = [];
|
|
37
|
+
|
|
38
|
+
lines.push('# E2E Test Learnings');
|
|
39
|
+
lines.push('');
|
|
40
|
+
lines.push(`> Auto-generated after each test run. Analysis window: **${days} days**.`);
|
|
41
|
+
lines.push(`> Last updated: ${summary.updatedAt || 'never'}`);
|
|
42
|
+
lines.push('');
|
|
43
|
+
|
|
44
|
+
// ── Health Overview ─────────────────────────────────────────────────────────
|
|
45
|
+
lines.push('## Health Overview');
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push('| Metric | Value |');
|
|
48
|
+
lines.push('|--------|-------|');
|
|
49
|
+
lines.push(`| Total Runs | ${summary.totalRuns} |`);
|
|
50
|
+
lines.push(`| Total Tests | ${summary.totalTests} |`);
|
|
51
|
+
lines.push(`| Pass Rate | ${summary.overallPassRate}% |`);
|
|
52
|
+
lines.push(`| Avg Duration | ${formatDuration(summary.avgDurationMs)} |`);
|
|
53
|
+
lines.push(`| Flaky Tests | ${flaky.length} |`);
|
|
54
|
+
lines.push(`| Unstable Selectors | ${selectors.length} |`);
|
|
55
|
+
|
|
56
|
+
// Trend arrow (compare last 2 days)
|
|
57
|
+
if (trends.length >= 2) {
|
|
58
|
+
const latest = trends[trends.length - 1];
|
|
59
|
+
const prev = trends[trends.length - 2];
|
|
60
|
+
const diff = latest.pass_rate - prev.pass_rate;
|
|
61
|
+
const arrow = diff > 0 ? 'improving' : diff < 0 ? 'declining' : 'stable';
|
|
62
|
+
lines.push(`| 7-Day Trend | ${arrow} (${diff > 0 ? '+' : ''}${diff.toFixed(1)}%) |`);
|
|
63
|
+
}
|
|
64
|
+
lines.push('');
|
|
65
|
+
|
|
66
|
+
// ── Flaky Tests ─────────────────────────────────────────────────────────────
|
|
67
|
+
if (flaky.length > 0) {
|
|
68
|
+
lines.push('## Flaky Tests');
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push('Tests that pass only after retries — potential stability issues.');
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push('| Test | Flaky Rate | Occurrences | Total Runs | Last Flaky | Avg Attempts |');
|
|
73
|
+
lines.push('|------|-----------|-------------|------------|------------|-------------|');
|
|
74
|
+
for (const f of flaky) {
|
|
75
|
+
lines.push(`| ${f.test_name} | ${f.flaky_rate}% | ${f.flaky_count} | ${f.total_runs} | ${formatDate(f.last_flaky)} | ${f.avg_attempts} |`);
|
|
76
|
+
}
|
|
77
|
+
lines.push('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Unstable Selectors ──────────────────────────────────────────────────────
|
|
81
|
+
if (selectors.length > 0) {
|
|
82
|
+
lines.push('## Unstable Selectors');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('CSS selectors that fail intermittently — candidates for improvement.');
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push('| Selector | Action | Fail Rate | Uses | Tests | Page | Error |');
|
|
87
|
+
lines.push('|----------|--------|-----------|------|-------|------|-------|');
|
|
88
|
+
for (const s of selectors.slice(0, 20)) {
|
|
89
|
+
const selector = truncate(s.selector, 40);
|
|
90
|
+
const error = truncate(s.last_error || '-', 30);
|
|
91
|
+
lines.push(`| \`${selector}\` | ${s.action_type} | ${s.fail_rate}% | ${s.total_uses} | ${s.used_by_tests} | ${s.page_url || '-'} | ${error} |`);
|
|
92
|
+
}
|
|
93
|
+
lines.push('');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Failing Pages ───────────────────────────────────────────────────────────
|
|
97
|
+
const failingPages = pages.filter(p => p.fail_rate > 0);
|
|
98
|
+
if (failingPages.length > 0) {
|
|
99
|
+
lines.push('## Failing Pages');
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push('| Page | Fail Rate | Visits | Tests | Console Errors | Network Errors | Avg Load |');
|
|
102
|
+
lines.push('|------|-----------|--------|-------|---------------|----------------|----------|');
|
|
103
|
+
for (const p of failingPages.slice(0, 20)) {
|
|
104
|
+
lines.push(`| ${p.url_path} | ${p.fail_rate}% | ${p.total_visits} | ${p.tested_by} | ${p.console_errors} | ${p.network_errors} | ${formatDuration(p.avg_load_ms)} |`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── API Issues ──────────────────────────────────────────────────────────────
|
|
110
|
+
const apiIssues = apis.filter(a => a.error_rate > 0);
|
|
111
|
+
if (apiIssues.length > 0) {
|
|
112
|
+
lines.push('## API Issues');
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push('| Endpoint | Error Rate | Calls | Avg Duration | Max Duration | Status Codes |');
|
|
115
|
+
lines.push('|----------|-----------|-------|-------------|-------------|-------------|');
|
|
116
|
+
for (const a of apiIssues.slice(0, 20)) {
|
|
117
|
+
lines.push(`| ${truncate(a.endpoint, 40)} | ${a.error_rate}% | ${a.total_calls} | ${formatDuration(a.avg_duration_ms)} | ${formatDuration(a.max_duration_ms)} | ${a.status_codes || '-'} |`);
|
|
118
|
+
}
|
|
119
|
+
lines.push('');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Error Patterns ──────────────────────────────────────────────────────────
|
|
123
|
+
if (errors.length > 0) {
|
|
124
|
+
lines.push('## Error Patterns');
|
|
125
|
+
lines.push('');
|
|
126
|
+
lines.push('| Pattern | Category | Count | First Seen | Last Seen | Example Test |');
|
|
127
|
+
lines.push('|---------|----------|-------|------------|-----------|-------------|');
|
|
128
|
+
for (const e of errors.slice(0, 20)) {
|
|
129
|
+
lines.push(`| ${truncate(e.pattern, 50)} | ${e.category} | ${e.occurrence_count} | ${formatDate(e.first_seen)} | ${formatDate(e.last_seen)} | ${e.example_test || '-'} |`);
|
|
130
|
+
}
|
|
131
|
+
lines.push('');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Recent Trend ────────────────────────────────────────────────────────────
|
|
135
|
+
if (trends.length > 0) {
|
|
136
|
+
const label = trendsGranularity === 'hourly' ? 'Recent Trend (hourly)' : 'Recent Trend (7 days)';
|
|
137
|
+
const col1 = trendsGranularity === 'hourly' ? 'Hour' : 'Date';
|
|
138
|
+
lines.push(`## ${label}`);
|
|
139
|
+
lines.push('');
|
|
140
|
+
lines.push(`| ${col1} | Pass Rate | Tests | Passed | Failed | Flaky | Avg Duration |`);
|
|
141
|
+
lines.push('|------|-----------|-------|--------|--------|-------|-------------|');
|
|
142
|
+
for (const t of trends) {
|
|
143
|
+
lines.push(`| ${t.date} | ${t.pass_rate}% | ${t.total_tests} | ${t.passed} | ${t.failed} | ${t.flaky_count} | ${formatDuration(t.avg_duration_ms)} |`);
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Write the file
|
|
149
|
+
const cwd = config?._cwd || process.cwd();
|
|
150
|
+
const e2eDir = path.join(cwd, 'e2e');
|
|
151
|
+
if (!fs.existsSync(e2eDir)) {
|
|
152
|
+
fs.mkdirSync(e2eDir, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const mdPath = path.join(e2eDir, 'learnings.md');
|
|
156
|
+
fs.writeFileSync(mdPath, lines.join('\n') + '\n');
|
|
157
|
+
|
|
158
|
+
return mdPath;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function formatDuration(ms) {
|
|
164
|
+
if (ms == null || isNaN(ms)) return '-';
|
|
165
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
166
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatDate(dateStr) {
|
|
170
|
+
if (!dateStr) return '-';
|
|
171
|
+
return dateStr.split('T')[0] || dateStr.slice(0, 10);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function truncate(str, max) {
|
|
175
|
+
if (!str) return '-';
|
|
176
|
+
return str.length > max ? str.slice(0, max - 3) + '...' : str;
|
|
177
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neo4j knowledge graph integration for the learning system.
|
|
3
|
+
*
|
|
4
|
+
* Optional — all operations are no-ops if neo4j-driver is not installed.
|
|
5
|
+
* Enable with learningsNeo4j: true in config.
|
|
6
|
+
*
|
|
7
|
+
* Nodes: Project, Test, Page, Selector, ApiEndpoint, ErrorPattern, Run
|
|
8
|
+
* Relationships: VISITS, USES_SELECTOR, CALLS_API, FAILED_WITH, EXECUTED_IN, SELECTOR_ON
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let neo4j = null;
|
|
12
|
+
let driver = null;
|
|
13
|
+
|
|
14
|
+
/** Try to load neo4j-driver. Returns false if not available. */
|
|
15
|
+
async function ensureDriver(config) {
|
|
16
|
+
if (driver) return true;
|
|
17
|
+
if (neo4j === false) return false; // already tried and failed
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
neo4j = (await import('neo4j-driver')).default;
|
|
21
|
+
driver = neo4j.driver(
|
|
22
|
+
config.neo4jBoltUrl || 'bolt://localhost:7687',
|
|
23
|
+
neo4j.auth.basic(config.neo4jUser || 'neo4j', config.neo4jPassword || 'e2erunner')
|
|
24
|
+
);
|
|
25
|
+
// Verify connectivity
|
|
26
|
+
await driver.verifyConnectivity();
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
neo4j = false;
|
|
30
|
+
driver = null;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Close the Neo4j driver. */
|
|
36
|
+
export async function closeNeo4j() {
|
|
37
|
+
if (driver) {
|
|
38
|
+
await driver.close();
|
|
39
|
+
driver = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Writes learning data to Neo4j graph.
|
|
45
|
+
* Called from learnFromRun() when learningsNeo4j is enabled.
|
|
46
|
+
* All operations are no-ops if neo4j-driver is not installed or connection fails.
|
|
47
|
+
*/
|
|
48
|
+
export async function writeToGraph(projectId, runDbId, report, config, suiteName) {
|
|
49
|
+
if (!config?.learningsNeo4j) return;
|
|
50
|
+
const connected = await ensureDriver(config);
|
|
51
|
+
if (!connected) return;
|
|
52
|
+
|
|
53
|
+
const session = driver.session();
|
|
54
|
+
try {
|
|
55
|
+
const projectName = config.projectName || 'unknown';
|
|
56
|
+
const cwd = config._cwd || process.cwd();
|
|
57
|
+
|
|
58
|
+
// Ensure Project node
|
|
59
|
+
await session.run(
|
|
60
|
+
'MERGE (p:Project {cwd: $cwd}) SET p.name = $name, p.updatedAt = datetime()',
|
|
61
|
+
{ cwd, name: projectName }
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Ensure Run node
|
|
65
|
+
await session.run(
|
|
66
|
+
`MERGE (r:Run {dbId: $runDbId})
|
|
67
|
+
SET r.total = $total, r.passed = $passed, r.failed = $failed,
|
|
68
|
+
r.passRate = $passRate, r.duration = $duration, r.suiteName = $suiteName,
|
|
69
|
+
r.createdAt = datetime()
|
|
70
|
+
WITH r
|
|
71
|
+
MATCH (p:Project {cwd: $cwd})
|
|
72
|
+
MERGE (r)-[:EXECUTED_IN]->(p)`,
|
|
73
|
+
{
|
|
74
|
+
runDbId: neo4j.int(runDbId),
|
|
75
|
+
total: neo4j.int(report.summary.total),
|
|
76
|
+
passed: neo4j.int(report.summary.passed),
|
|
77
|
+
failed: neo4j.int(report.summary.failed),
|
|
78
|
+
passRate: report.summary.passRate,
|
|
79
|
+
duration: report.summary.duration,
|
|
80
|
+
suiteName: suiteName || null,
|
|
81
|
+
cwd,
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const result of report.results) {
|
|
86
|
+
// Test node
|
|
87
|
+
await session.run(
|
|
88
|
+
`MERGE (t:Test {name: $name, projectCwd: $cwd})
|
|
89
|
+
SET t.lastSuccess = $success, t.lastDuration = $duration, t.updatedAt = datetime()
|
|
90
|
+
WITH t
|
|
91
|
+
MATCH (r:Run {dbId: $runDbId})
|
|
92
|
+
MERGE (t)-[:EXECUTED_IN]->(r)`,
|
|
93
|
+
{
|
|
94
|
+
name: result.name,
|
|
95
|
+
cwd,
|
|
96
|
+
success: result.success,
|
|
97
|
+
duration: result.endTime && result.startTime
|
|
98
|
+
? new Date(result.endTime) - new Date(result.startTime)
|
|
99
|
+
: 0,
|
|
100
|
+
runDbId: neo4j.int(runDbId),
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Page nodes + VISITS relationships
|
|
105
|
+
if (result.actions) {
|
|
106
|
+
for (const action of result.actions) {
|
|
107
|
+
if ((action.type === 'goto' || action.type === 'navigate') && action.value) {
|
|
108
|
+
let urlPath = action.value;
|
|
109
|
+
try { urlPath = new URL(action.value, 'http://placeholder').pathname; } catch { /* */ }
|
|
110
|
+
|
|
111
|
+
await session.run(
|
|
112
|
+
`MERGE (pg:Page {path: $path, projectCwd: $cwd})
|
|
113
|
+
SET pg.updatedAt = datetime()
|
|
114
|
+
WITH pg
|
|
115
|
+
MATCH (t:Test {name: $testName, projectCwd: $cwd})
|
|
116
|
+
MERGE (t)-[:VISITS]->(pg)`,
|
|
117
|
+
{ path: urlPath, cwd, testName: result.name }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Selector nodes + USES_SELECTOR relationships
|
|
122
|
+
if (action.selector) {
|
|
123
|
+
await session.run(
|
|
124
|
+
`MERGE (s:Selector {value: $selector, projectCwd: $cwd})
|
|
125
|
+
SET s.updatedAt = datetime()
|
|
126
|
+
WITH s
|
|
127
|
+
MATCH (t:Test {name: $testName, projectCwd: $cwd})
|
|
128
|
+
MERGE (t)-[:USES_SELECTOR {actionType: $actionType}]->(s)`,
|
|
129
|
+
{ selector: action.selector, cwd, testName: result.name, actionType: action.type }
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// API endpoint nodes + CALLS_API relationships
|
|
136
|
+
if (result.networkLogs?.length) {
|
|
137
|
+
for (const log of result.networkLogs) {
|
|
138
|
+
if (!log.url || !log.method) continue;
|
|
139
|
+
let urlPath;
|
|
140
|
+
try { urlPath = new URL(log.url).pathname; } catch { urlPath = log.url; }
|
|
141
|
+
urlPath = urlPath
|
|
142
|
+
.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '/:uuid')
|
|
143
|
+
.replace(/\/\d+/g, '/:id');
|
|
144
|
+
const endpoint = `${log.method} ${urlPath}`;
|
|
145
|
+
|
|
146
|
+
await session.run(
|
|
147
|
+
`MERGE (a:ApiEndpoint {endpoint: $endpoint, projectCwd: $cwd})
|
|
148
|
+
SET a.updatedAt = datetime()
|
|
149
|
+
WITH a
|
|
150
|
+
MATCH (t:Test {name: $testName, projectCwd: $cwd})
|
|
151
|
+
MERGE (t)-[:CALLS_API]->(a)`,
|
|
152
|
+
{ endpoint, cwd, testName: result.name }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Error pattern nodes + FAILED_WITH relationships
|
|
158
|
+
if (result.error) {
|
|
159
|
+
const normalized = result.error.replace(/\d+ms/g, 'Nms').replace(/"[^"]+"/g, '"..."').slice(0, 200);
|
|
160
|
+
await session.run(
|
|
161
|
+
`MERGE (e:ErrorPattern {pattern: $pattern, projectCwd: $cwd})
|
|
162
|
+
SET e.count = COALESCE(e.count, 0) + 1, e.lastSeen = datetime()
|
|
163
|
+
WITH e
|
|
164
|
+
MATCH (t:Test {name: $testName, projectCwd: $cwd})
|
|
165
|
+
MERGE (t)-[:FAILED_WITH]->(e)`,
|
|
166
|
+
{ pattern: normalized, cwd, testName: result.name }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
await session.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Query the graph for relationships — used by e2e_learnings MCP tool.
|
|
177
|
+
* Returns enriched insights about test dependencies, shared selectors, etc.
|
|
178
|
+
*/
|
|
179
|
+
export async function queryGraph(config, queryType, params = {}) {
|
|
180
|
+
if (!config?.learningsNeo4j) return null;
|
|
181
|
+
const connected = await ensureDriver(config);
|
|
182
|
+
if (!connected) return null;
|
|
183
|
+
|
|
184
|
+
const session = driver.session();
|
|
185
|
+
const cwd = config._cwd || process.cwd();
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
switch (queryType) {
|
|
189
|
+
case 'test-dependencies': {
|
|
190
|
+
// Tests that share selectors or pages with a given test
|
|
191
|
+
const result = await session.run(
|
|
192
|
+
`MATCH (t1:Test {name: $testName, projectCwd: $cwd})-[:USES_SELECTOR]->(s)<-[:USES_SELECTOR]-(t2:Test)
|
|
193
|
+
WHERE t1 <> t2
|
|
194
|
+
RETURN DISTINCT t2.name AS related, COLLECT(DISTINCT s.value) AS sharedSelectors
|
|
195
|
+
LIMIT 20`,
|
|
196
|
+
{ testName: params.testName, cwd }
|
|
197
|
+
);
|
|
198
|
+
return result.records.map(r => ({
|
|
199
|
+
test: r.get('related'),
|
|
200
|
+
sharedSelectors: r.get('sharedSelectors'),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case 'page-impact': {
|
|
205
|
+
// All tests that visit a given page
|
|
206
|
+
const result = await session.run(
|
|
207
|
+
`MATCH (t:Test {projectCwd: $cwd})-[:VISITS]->(pg:Page {path: $path, projectCwd: $cwd})
|
|
208
|
+
RETURN t.name AS test, t.lastSuccess AS lastSuccess`,
|
|
209
|
+
{ path: params.path, cwd }
|
|
210
|
+
);
|
|
211
|
+
return result.records.map(r => ({
|
|
212
|
+
test: r.get('test'),
|
|
213
|
+
lastSuccess: r.get('lastSuccess'),
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case 'error-impact': {
|
|
218
|
+
// Tests that failed with a given error pattern
|
|
219
|
+
const result = await session.run(
|
|
220
|
+
`MATCH (t:Test {projectCwd: $cwd})-[:FAILED_WITH]->(e:ErrorPattern)
|
|
221
|
+
WHERE e.pattern CONTAINS $search
|
|
222
|
+
RETURN t.name AS test, e.pattern AS pattern, e.count AS count
|
|
223
|
+
ORDER BY e.count DESC
|
|
224
|
+
LIMIT 20`,
|
|
225
|
+
{ search: params.search || '', cwd }
|
|
226
|
+
);
|
|
227
|
+
return result.records.map(r => ({
|
|
228
|
+
test: r.get('test'),
|
|
229
|
+
pattern: r.get('pattern'),
|
|
230
|
+
count: r.get('count')?.toNumber?.() || r.get('count'),
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'selector-usage': {
|
|
235
|
+
// All tests and pages using a given selector
|
|
236
|
+
const result = await session.run(
|
|
237
|
+
`MATCH (t:Test {projectCwd: $cwd})-[r:USES_SELECTOR]->(s:Selector {value: $selector, projectCwd: $cwd})
|
|
238
|
+
OPTIONAL MATCH (t)-[:VISITS]->(pg:Page)
|
|
239
|
+
RETURN t.name AS test, r.actionType AS action, COLLECT(DISTINCT pg.path) AS pages`,
|
|
240
|
+
{ selector: params.selector, cwd }
|
|
241
|
+
);
|
|
242
|
+
return result.records.map(r => ({
|
|
243
|
+
test: r.get('test'),
|
|
244
|
+
action: r.get('action'),
|
|
245
|
+
pages: r.get('pages'),
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
default:
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
} finally {
|
|
253
|
+
await session.close();
|
|
254
|
+
}
|
|
255
|
+
}
|