@matware/e2e-runner 1.1.1 → 1.3.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/.claude-plugin/marketplace.json +21 -0
- package/.claude-plugin/plugin.json +9 -0
- package/.mcp.json +9 -0
- package/.opencode/commands/create-test.md +63 -0
- package/.opencode/commands/run.md +50 -0
- package/.opencode/commands/verify-issue.md +62 -0
- package/.opencode/skills/e2e-testing/SKILL.md +181 -0
- package/.opencode/skills/e2e-testing/references/action-types.md +143 -0
- package/.opencode/skills/e2e-testing/references/auth-strategies.md +91 -0
- package/.opencode/skills/e2e-testing/references/graphql.md +59 -0
- package/.opencode/skills/e2e-testing/references/issue-verification.md +59 -0
- package/.opencode/skills/e2e-testing/references/multi-pool.md +60 -0
- package/.opencode/skills/e2e-testing/references/network-debugging.md +62 -0
- package/.opencode/skills/e2e-testing/references/test-json-format.md +163 -0
- package/.opencode/skills/e2e-testing/references/troubleshooting.md +224 -0
- package/.opencode/skills/e2e-testing/references/variables.md +41 -0
- package/.opencode/skills/e2e-testing/references/visual-verification.md +89 -0
- package/OPENCODE.md +166 -0
- package/README.md +990 -296
- package/agents/test-analyzer.md +81 -0
- package/agents/test-creator.md +155 -0
- package/agents/test-improver.md +177 -0
- package/bin/cli.js +602 -22
- package/commands/create-test.md +65 -0
- package/commands/run.md +49 -0
- package/commands/verify-issue.md +63 -0
- package/opencode.json +11 -0
- package/package.json +15 -2
- package/scripts/setup-opencode.sh +113 -0
- package/skills/e2e-testing/SKILL.md +173 -0
- package/skills/e2e-testing/references/action-types.md +143 -0
- package/skills/e2e-testing/references/auth-strategies.md +91 -0
- package/skills/e2e-testing/references/graphql.md +59 -0
- package/skills/e2e-testing/references/issue-verification.md +59 -0
- package/skills/e2e-testing/references/multi-pool.md +60 -0
- package/skills/e2e-testing/references/network-debugging.md +62 -0
- package/skills/e2e-testing/references/test-json-format.md +163 -0
- package/skills/e2e-testing/references/troubleshooting.md +224 -0
- package/skills/e2e-testing/references/variables.md +41 -0
- package/skills/e2e-testing/references/visual-verification.md +89 -0
- package/src/actions.js +597 -20
- package/src/ai-generate.js +142 -12
- package/src/config.js +171 -0
- package/src/dashboard.js +299 -17
- package/src/db.js +335 -13
- package/src/index.js +15 -8
- package/src/learner-markdown.js +177 -0
- package/src/learner-neo4j.js +255 -0
- package/src/learner-sqlite.js +658 -0
- package/src/learner.js +418 -0
- package/src/mcp-tools.js +1558 -50
- package/src/module-resolver.js +310 -0
- package/src/narrate.js +262 -0
- package/src/neo4j-pool.js +124 -0
- package/src/pool-manager.js +223 -0
- package/src/reporter.js +117 -3
- package/src/runner.js +274 -71
- package/src/sync/auth.js +354 -0
- package/src/sync/client.js +572 -0
- package/src/sync/hub-routes.js +816 -0
- package/src/sync/index.js +68 -0
- package/src/sync/middleware.js +347 -0
- package/src/sync/queue.js +209 -0
- package/src/sync/schema.js +540 -0
- package/src/verify.js +14 -9
- package/src/watch.js +384 -0
- package/templates/build-dashboard.js +69 -0
- package/templates/dashboard/js/api.js +60 -0
- package/templates/dashboard/js/init.js +13 -0
- package/templates/dashboard/js/keyboard.js +46 -0
- package/templates/dashboard/js/state.js +40 -0
- package/templates/dashboard/js/toast.js +41 -0
- package/templates/dashboard/js/utils.js +196 -0
- package/templates/dashboard/js/view-live.js +143 -0
- package/templates/dashboard/js/view-runs.js +572 -0
- package/templates/dashboard/js/view-tests.js +294 -0
- package/templates/dashboard/js/view-watch.js +242 -0
- package/templates/dashboard/js/websocket.js +110 -0
- package/templates/dashboard/styles/base.css +69 -0
- package/templates/dashboard/styles/components.css +110 -0
- package/templates/dashboard/styles/view-live.css +74 -0
- package/templates/dashboard/styles/view-runs.css +207 -0
- package/templates/dashboard/styles/view-tests.css +96 -0
- package/templates/dashboard/styles/view-watch.css +53 -0
- package/templates/dashboard/template.html +267 -0
- package/templates/dashboard.html +2171 -530
- package/templates/docker-compose-neo4j.yml +19 -0
- package/templates/e2e.config.js +3 -0
- package/templates/sample-test.json +0 -8
|
@@ -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
|
+
}
|