@matware/e2e-runner 1.2.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/.mcp.json +2 -2
- 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 +581 -55
- package/agents/test-creator.md +54 -1
- package/agents/test-improver.md +37 -0
- package/bin/cli.js +408 -16
- package/commands/create-test.md +16 -1
- package/opencode.json +11 -0
- package/package.json +7 -2
- package/scripts/setup-opencode.sh +113 -0
- package/skills/e2e-testing/SKILL.md +10 -3
- package/skills/e2e-testing/references/action-types.md +48 -5
- 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 +4 -0
- package/skills/e2e-testing/references/troubleshooting.md +44 -2
- package/skills/e2e-testing/references/variables.md +41 -0
- package/skills/e2e-testing/references/visual-verification.md +89 -0
- package/src/actions.js +324 -2
- package/src/ai-generate.js +58 -8
- package/src/config.js +143 -0
- package/src/dashboard.js +145 -13
- package/src/db.js +130 -2
- package/src/index.js +7 -6
- package/src/learner-sqlite.js +304 -0
- package/src/learner.js +8 -3
- package/src/mcp-tools.js +1121 -43
- package/src/module-resolver.js +37 -0
- package/src/narrate.js +37 -0
- package/src/pool-manager.js +223 -0
- package/src/reporter.js +82 -1
- package/src/runner.js +157 -28
- 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 +10 -7
- package/src/watch.js +384 -0
- package/templates/build-dashboard.js +47 -6
- 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 +165 -99
- package/templates/dashboard.html +1596 -541
- package/templates/sample-test.json +0 -8
- package/templates/dashboard/app.js +0 -1152
- package/templates/dashboard/styles.css +0 -413
package/src/config.js
CHANGED
|
@@ -12,6 +12,22 @@ import fs from 'fs';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { pathToFileURL } from 'url';
|
|
14
14
|
|
|
15
|
+
/** Deep merge utility for nested config objects */
|
|
16
|
+
function deepMerge(...objects) {
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const obj of objects) {
|
|
19
|
+
if (!obj || typeof obj !== 'object') continue;
|
|
20
|
+
for (const key of Object.keys(obj)) {
|
|
21
|
+
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
|
22
|
+
result[key] = deepMerge(result[key] || {}, obj[key]);
|
|
23
|
+
} else if (obj[key] !== undefined) {
|
|
24
|
+
result[key] = obj[key];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
const DEFAULTS = {
|
|
16
32
|
baseUrl: 'http://host.docker.internal:3000',
|
|
17
33
|
poolUrl: 'ws://localhost:3333',
|
|
@@ -51,12 +67,65 @@ const DEFAULTS = {
|
|
|
51
67
|
neo4jPassword: 'e2erunner',
|
|
52
68
|
neo4jBoltPort: 7687,
|
|
53
69
|
neo4jHttpPort: 7474,
|
|
70
|
+
verificationStrictness: 'moderate',
|
|
71
|
+
networkIgnoreDomains: [],
|
|
72
|
+
authLoginEndpoint: null,
|
|
73
|
+
authCredentials: null,
|
|
74
|
+
authTokenPath: 'token',
|
|
75
|
+
gqlEndpoint: '/api/graphql',
|
|
76
|
+
gqlAuthHeader: 'Authorization',
|
|
77
|
+
gqlAuthKey: 'accessToken',
|
|
78
|
+
gqlAuthPrefix: 'Bearer ',
|
|
79
|
+
poolUrls: null,
|
|
80
|
+
watchInterval: null,
|
|
81
|
+
watchRunOnStart: true,
|
|
82
|
+
watchGitPoll: false,
|
|
83
|
+
watchGitBranch: null,
|
|
84
|
+
watchGitInterval: '30s',
|
|
85
|
+
watchWebhookUrl: null,
|
|
86
|
+
watchWebhookEvents: 'failure',
|
|
87
|
+
watchProjects: null,
|
|
88
|
+
|
|
89
|
+
// Sync configuration
|
|
90
|
+
sync: {
|
|
91
|
+
mode: 'standalone', // 'standalone' | 'hub' | 'agent'
|
|
92
|
+
hub: {
|
|
93
|
+
port: null, // null = use dashboardPort
|
|
94
|
+
tls: {
|
|
95
|
+
enabled: false,
|
|
96
|
+
certPath: null,
|
|
97
|
+
keyPath: null,
|
|
98
|
+
mtls: false,
|
|
99
|
+
caPath: null,
|
|
100
|
+
},
|
|
101
|
+
allowRegistration: true,
|
|
102
|
+
requireApproval: false,
|
|
103
|
+
masterKeyEnv: 'E2E_SYNC_MASTER_KEY',
|
|
104
|
+
},
|
|
105
|
+
agent: {
|
|
106
|
+
hubUrl: null,
|
|
107
|
+
instanceId: null,
|
|
108
|
+
displayName: null,
|
|
109
|
+
apiKeyEnv: 'E2E_SYNC_API_KEY',
|
|
110
|
+
totpSecretEnv: 'E2E_SYNC_TOTP',
|
|
111
|
+
tls: {
|
|
112
|
+
certPath: null,
|
|
113
|
+
keyPath: null,
|
|
114
|
+
caPath: null,
|
|
115
|
+
},
|
|
116
|
+
autoSync: true,
|
|
117
|
+
pullOnDashboard: true,
|
|
118
|
+
offlineQueue: true,
|
|
119
|
+
queueRetryInterval: 60,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
54
122
|
};
|
|
55
123
|
|
|
56
124
|
function loadEnvVars() {
|
|
57
125
|
const env = {};
|
|
58
126
|
if (process.env.BASE_URL) env.baseUrl = process.env.BASE_URL;
|
|
59
127
|
if (process.env.CHROME_POOL_URL) env.poolUrl = process.env.CHROME_POOL_URL;
|
|
128
|
+
if (process.env.CHROME_POOL_URLS) env.poolUrls = process.env.CHROME_POOL_URLS.split(',').map(u => u.trim()).filter(Boolean);
|
|
60
129
|
if (process.env.TESTS_DIR) env.testsDir = process.env.TESTS_DIR;
|
|
61
130
|
if (process.env.MODULES_DIR) env.modulesDir = process.env.MODULES_DIR;
|
|
62
131
|
if (process.env.SCREENSHOTS_DIR) env.screenshotsDir = process.env.SCREENSHOTS_DIR;
|
|
@@ -86,6 +155,61 @@ function loadEnvVars() {
|
|
|
86
155
|
if (process.env.NEO4J_PASSWORD) env.neo4jPassword = process.env.NEO4J_PASSWORD;
|
|
87
156
|
if (process.env.NEO4J_BOLT_PORT) env.neo4jBoltPort = parseInt(process.env.NEO4J_BOLT_PORT);
|
|
88
157
|
if (process.env.NEO4J_HTTP_PORT) env.neo4jHttpPort = parseInt(process.env.NEO4J_HTTP_PORT);
|
|
158
|
+
if (process.env.NETWORK_IGNORE_DOMAINS) env.networkIgnoreDomains = process.env.NETWORK_IGNORE_DOMAINS.split(',').map(d => d.trim()).filter(Boolean);
|
|
159
|
+
if (process.env.AUTH_LOGIN_ENDPOINT) env.authLoginEndpoint = process.env.AUTH_LOGIN_ENDPOINT;
|
|
160
|
+
if (process.env.AUTH_TOKEN_PATH) env.authTokenPath = process.env.AUTH_TOKEN_PATH;
|
|
161
|
+
if (process.env.GQL_ENDPOINT) env.gqlEndpoint = process.env.GQL_ENDPOINT;
|
|
162
|
+
if (process.env.GQL_AUTH_HEADER) env.gqlAuthHeader = process.env.GQL_AUTH_HEADER;
|
|
163
|
+
if (process.env.GQL_AUTH_KEY) env.gqlAuthKey = process.env.GQL_AUTH_KEY;
|
|
164
|
+
if (process.env.GQL_AUTH_PREFIX) env.gqlAuthPrefix = process.env.GQL_AUTH_PREFIX;
|
|
165
|
+
if (process.env.WATCH_INTERVAL) env.watchInterval = process.env.WATCH_INTERVAL;
|
|
166
|
+
if (process.env.WATCH_WEBHOOK_URL) env.watchWebhookUrl = process.env.WATCH_WEBHOOK_URL;
|
|
167
|
+
if (process.env.WATCH_WEBHOOK_EVENTS) env.watchWebhookEvents = process.env.WATCH_WEBHOOK_EVENTS;
|
|
168
|
+
if (process.env.WATCH_GIT_POLL) env.watchGitPoll = process.env.WATCH_GIT_POLL === 'true' || process.env.WATCH_GIT_POLL === '1';
|
|
169
|
+
if (process.env.WATCH_GIT_BRANCH) env.watchGitBranch = process.env.WATCH_GIT_BRANCH;
|
|
170
|
+
if (process.env.WATCH_GIT_INTERVAL) env.watchGitInterval = process.env.WATCH_GIT_INTERVAL;
|
|
171
|
+
if (process.env.VERIFICATION_STRICTNESS) {
|
|
172
|
+
const val = process.env.VERIFICATION_STRICTNESS.toLowerCase();
|
|
173
|
+
if (['strict', 'moderate', 'lenient'].includes(val)) {
|
|
174
|
+
env.verificationStrictness = val;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Sync configuration from env vars
|
|
179
|
+
if (process.env.E2E_SYNC_MODE) {
|
|
180
|
+
const mode = process.env.E2E_SYNC_MODE.toLowerCase();
|
|
181
|
+
if (['standalone', 'hub', 'agent'].includes(mode)) {
|
|
182
|
+
env.sync = env.sync || {};
|
|
183
|
+
env.sync.mode = mode;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (process.env.E2E_SYNC_HUB_URL) {
|
|
187
|
+
env.sync = env.sync || {};
|
|
188
|
+
env.sync.agent = env.sync.agent || {};
|
|
189
|
+
env.sync.agent.hubUrl = process.env.E2E_SYNC_HUB_URL;
|
|
190
|
+
}
|
|
191
|
+
if (process.env.E2E_SYNC_INSTANCE_ID) {
|
|
192
|
+
env.sync = env.sync || {};
|
|
193
|
+
env.sync.agent = env.sync.agent || {};
|
|
194
|
+
env.sync.agent.instanceId = process.env.E2E_SYNC_INSTANCE_ID;
|
|
195
|
+
}
|
|
196
|
+
if (process.env.E2E_SYNC_DISPLAY_NAME) {
|
|
197
|
+
env.sync = env.sync || {};
|
|
198
|
+
env.sync.agent = env.sync.agent || {};
|
|
199
|
+
env.sync.agent.displayName = process.env.E2E_SYNC_DISPLAY_NAME;
|
|
200
|
+
}
|
|
201
|
+
if (process.env.E2E_SYNC_HUB_PORT) {
|
|
202
|
+
env.sync = env.sync || {};
|
|
203
|
+
env.sync.hub = env.sync.hub || {};
|
|
204
|
+
env.sync.hub.port = parseInt(process.env.E2E_SYNC_HUB_PORT);
|
|
205
|
+
}
|
|
206
|
+
if (process.env.E2E_SYNC_TLS_ENABLED) {
|
|
207
|
+
env.sync = env.sync || {};
|
|
208
|
+
env.sync.hub = env.sync.hub || {};
|
|
209
|
+
env.sync.hub.tls = env.sync.hub.tls || {};
|
|
210
|
+
env.sync.hub.tls.enabled = process.env.E2E_SYNC_TLS_ENABLED === 'true' || process.env.E2E_SYNC_TLS_ENABLED === '1';
|
|
211
|
+
}
|
|
212
|
+
|
|
89
213
|
return env;
|
|
90
214
|
}
|
|
91
215
|
|
|
@@ -142,6 +266,16 @@ export async function loadConfig(cliArgs = {}, cwd = null) {
|
|
|
142
266
|
...envConfig,
|
|
143
267
|
...cliArgs,
|
|
144
268
|
};
|
|
269
|
+
|
|
270
|
+
// Deep merge sync config (nested objects need special handling)
|
|
271
|
+
if (fileConfig.sync || envConfig.sync || cliArgs.sync) {
|
|
272
|
+
config.sync = deepMerge(
|
|
273
|
+
DEFAULTS.sync,
|
|
274
|
+
fileConfig.sync || {},
|
|
275
|
+
envConfig.sync || {},
|
|
276
|
+
cliArgs.sync || {}
|
|
277
|
+
);
|
|
278
|
+
}
|
|
145
279
|
|
|
146
280
|
// Apply environment profile overrides
|
|
147
281
|
if (config.env && config.env !== 'default' && config.environments?.[config.env]) {
|
|
@@ -172,5 +306,14 @@ export async function loadConfig(cliArgs = {}, cwd = null) {
|
|
|
172
306
|
config.projectName = path.basename(cwd);
|
|
173
307
|
}
|
|
174
308
|
|
|
309
|
+
// Normalize pool URLs: poolUrls array → _poolUrls, keep poolUrl as primary
|
|
310
|
+
if (config.poolUrls && Array.isArray(config.poolUrls) && config.poolUrls.length > 0) {
|
|
311
|
+
config._poolUrls = config.poolUrls;
|
|
312
|
+
config.poolUrl = config.poolUrls[0];
|
|
313
|
+
} else {
|
|
314
|
+
config._poolUrls = [config.poolUrl];
|
|
315
|
+
}
|
|
316
|
+
delete config.poolUrls;
|
|
317
|
+
|
|
175
318
|
return config;
|
|
176
319
|
}
|
package/src/dashboard.js
CHANGED
|
@@ -14,13 +14,15 @@ import path from 'path';
|
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
15
|
import { createRequire } from 'module';
|
|
16
16
|
import { createWebSocketServer } from './websocket.js';
|
|
17
|
-
import {
|
|
17
|
+
import { getPoolUrls, getAggregatedPoolStatus, waitForAnyPool } from './pool-manager.js';
|
|
18
18
|
import { runTestsParallel, loadAllSuites, loadTestSuite, listSuites } from './runner.js';
|
|
19
19
|
import { generateReport, generateJUnitXML, saveReport, persistRun, loadHistory, loadHistoryRun } from './reporter.js';
|
|
20
|
-
import { listProjects as dbListProjects, getProjectRuns as dbGetProjectRuns, getRunDetail as dbGetRunDetail, getAllRuns as dbGetAllRuns, getRunCount as dbGetRunCount, getProjectScreenshotsDir as dbGetProjectScreenshotsDir, getProjectTestsDir as dbGetProjectTestsDir, getProjectCwd as dbGetProjectCwd, lookupScreenshotHash as dbLookupScreenshotHash, ensureProject as dbEnsureProject, getNetworkLogs as dbGetNetworkLogs, closeDb } from './db.js';
|
|
20
|
+
import { listProjects as dbListProjects, listProjectsWithSparklines as dbListProjectsWithSparklines, getProjectRuns as dbGetProjectRuns, getRunDetail as dbGetRunDetail, getAllRuns as dbGetAllRuns, getRunCount as dbGetRunCount, getProjectScreenshotsDir as dbGetProjectScreenshotsDir, getProjectTestsDir as dbGetProjectTestsDir, getProjectCwd as dbGetProjectCwd, lookupScreenshotHash as dbLookupScreenshotHash, ensureProject as dbEnsureProject, getNetworkLogs as dbGetNetworkLogs, listVariables as dbListVariables, setVariable as dbSetVariable, deleteVariable as dbDeleteVariable, closeDb } from './db.js';
|
|
21
21
|
import { loadConfig } from './config.js';
|
|
22
22
|
import { log, colors as C } from './logger.js';
|
|
23
|
-
import { getLearningsSummary, getFlakySummary, getSelectorStability, getPageHealth, getApiHealth, getErrorPatterns, getTestTrends } from './learner-sqlite.js';
|
|
23
|
+
import { getLearningsSummary, getFlakySummary, getSelectorStability, getPageHealth, getApiHealth, getErrorPatterns, getTestTrends, getRunInsights, getHealthSnapshot } from './learner-sqlite.js';
|
|
24
|
+
import { handleSyncRoutes } from './sync/hub-routes.js';
|
|
25
|
+
import { migrateSyncSchema } from './sync/schema.js';
|
|
24
26
|
|
|
25
27
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
26
28
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
@@ -38,6 +40,12 @@ export async function startDashboard(config) {
|
|
|
38
40
|
const port = config.dashboardPort || 8484;
|
|
39
41
|
const MAX_BODY = 1024 * 1024; // 1MB limit for POST bodies
|
|
40
42
|
const dashboardHtml = fs.readFileSync(path.join(__dirname, '..', 'templates', 'dashboard.html'), 'utf-8');
|
|
43
|
+
|
|
44
|
+
// Migrate sync schema if in hub mode
|
|
45
|
+
if (config.sync?.mode === 'hub') {
|
|
46
|
+
migrateSyncSchema();
|
|
47
|
+
log(`${C.cyan}[sync]${C.reset} Hub mode enabled`);
|
|
48
|
+
}
|
|
41
49
|
|
|
42
50
|
let currentRun = null; // { running: true, runId, report } or null
|
|
43
51
|
let latestReport = null;
|
|
@@ -86,7 +94,7 @@ export async function startDashboard(config) {
|
|
|
86
94
|
if (origin && allowedOrigins.includes(origin)) {
|
|
87
95
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
88
96
|
}
|
|
89
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
97
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
90
98
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id');
|
|
91
99
|
|
|
92
100
|
if (req.method === 'OPTIONS') {
|
|
@@ -96,6 +104,12 @@ export async function startDashboard(config) {
|
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
try {
|
|
107
|
+
// Handle sync routes if in hub mode
|
|
108
|
+
if (config.sync?.mode === 'hub' && pathname.startsWith('/api/sync')) {
|
|
109
|
+
const handled = await handleSyncRoutes(req, res, config, pathname);
|
|
110
|
+
if (handled) return;
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
// Serve dashboard HTML
|
|
100
114
|
if (pathname === '/' || pathname === '/index.html') {
|
|
101
115
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -105,9 +119,11 @@ export async function startDashboard(config) {
|
|
|
105
119
|
|
|
106
120
|
// API: pool status + dashboard state
|
|
107
121
|
if (pathname === '/api/status') {
|
|
108
|
-
const
|
|
122
|
+
const poolUrls = getPoolUrls(config);
|
|
123
|
+
const aggregated = await getAggregatedPoolStatus(poolUrls);
|
|
109
124
|
jsonResponse(res, {
|
|
110
|
-
pool:
|
|
125
|
+
pool: aggregated,
|
|
126
|
+
poolUrls,
|
|
111
127
|
dashboard: {
|
|
112
128
|
running: currentRun?.running || false,
|
|
113
129
|
wsClients: wss.clientCount,
|
|
@@ -115,8 +131,10 @@ export async function startDashboard(config) {
|
|
|
115
131
|
config: {
|
|
116
132
|
baseUrl: config.baseUrl,
|
|
117
133
|
poolUrl: config.poolUrl,
|
|
134
|
+
poolUrls,
|
|
118
135
|
concurrency: config.concurrency,
|
|
119
136
|
testsDir: config.testsDir,
|
|
137
|
+
sync: config.sync || { mode: 'standalone' },
|
|
120
138
|
},
|
|
121
139
|
});
|
|
122
140
|
return;
|
|
@@ -162,6 +180,17 @@ export async function startDashboard(config) {
|
|
|
162
180
|
return;
|
|
163
181
|
}
|
|
164
182
|
|
|
183
|
+
// API: DB — projects overview with sparklines (Watch view)
|
|
184
|
+
if (pathname === '/api/db/projects/overview') {
|
|
185
|
+
try {
|
|
186
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
187
|
+
jsonResponse(res, dbListProjectsWithSparklines(limit));
|
|
188
|
+
} catch (error) {
|
|
189
|
+
jsonResponse(res, { error: error.message }, 500);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
165
194
|
// API: DB — runs for a project
|
|
166
195
|
const projectRunsMatch = pathname.match(/^\/api\/db\/projects\/(\d+)\/runs$/);
|
|
167
196
|
if (projectRunsMatch) {
|
|
@@ -226,6 +255,56 @@ export async function startDashboard(config) {
|
|
|
226
255
|
return;
|
|
227
256
|
}
|
|
228
257
|
|
|
258
|
+
// API: DB — run insights (health + contextual insights)
|
|
259
|
+
const runInsightsMatch = pathname.match(/^\/api\/db\/runs\/(\d+)\/insights$/);
|
|
260
|
+
if (runInsightsMatch) {
|
|
261
|
+
try {
|
|
262
|
+
const runDbId = parseInt(runInsightsMatch[1], 10);
|
|
263
|
+
const detail = dbGetRunDetail(runDbId);
|
|
264
|
+
if (!detail) { jsonResponse(res, { error: 'Run not found' }, 404); return; }
|
|
265
|
+
|
|
266
|
+
const projectId = detail.projectId || null;
|
|
267
|
+
const health = projectId ? getHealthSnapshot(projectId) : null;
|
|
268
|
+
|
|
269
|
+
// Build a minimal report object for getRunInsights
|
|
270
|
+
const results = (detail.results || []).map(r => ({
|
|
271
|
+
name: r.name,
|
|
272
|
+
success: r.success,
|
|
273
|
+
actions: r.actions || [],
|
|
274
|
+
}));
|
|
275
|
+
const insights = projectId ? getRunInsights(projectId, { results }) : [];
|
|
276
|
+
|
|
277
|
+
jsonResponse(res, { health, insights });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
jsonResponse(res, { error: error.message }, 500);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// API: DB — project health snapshot
|
|
285
|
+
const projectHealthMatch = pathname.match(/^\/api\/db\/projects\/(\d+)\/health$/);
|
|
286
|
+
if (projectHealthMatch) {
|
|
287
|
+
try {
|
|
288
|
+
const projectId = parseInt(projectHealthMatch[1], 10);
|
|
289
|
+
const health = getHealthSnapshot(projectId);
|
|
290
|
+
jsonResponse(res, health || {});
|
|
291
|
+
} catch (error) {
|
|
292
|
+
jsonResponse(res, { error: error.message }, 500);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// API: DB — cross-project health snapshot
|
|
298
|
+
if (pathname === '/api/db/health') {
|
|
299
|
+
try {
|
|
300
|
+
const health = getHealthSnapshot(null);
|
|
301
|
+
jsonResponse(res, health || {});
|
|
302
|
+
} catch (error) {
|
|
303
|
+
jsonResponse(res, { error: error.message }, 500);
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
229
308
|
// API: DB — project screenshots list
|
|
230
309
|
const projectScreenshotsMatch = pathname.match(/^\/api\/db\/projects\/(\d+)\/screenshots$/);
|
|
231
310
|
if (projectScreenshotsMatch) {
|
|
@@ -334,6 +413,57 @@ export async function startDashboard(config) {
|
|
|
334
413
|
return;
|
|
335
414
|
}
|
|
336
415
|
|
|
416
|
+
// API: DB — project variables (list)
|
|
417
|
+
const projectVarsMatch = pathname.match(/^\/api\/db\/projects\/(\d+)\/variables$/);
|
|
418
|
+
if (projectVarsMatch && req.method === 'GET') {
|
|
419
|
+
try {
|
|
420
|
+
const projectId = parseInt(projectVarsMatch[1], 10);
|
|
421
|
+
jsonResponse(res, dbListVariables(projectId));
|
|
422
|
+
} catch (error) {
|
|
423
|
+
jsonResponse(res, { error: error.message }, 500);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// API: DB — project variables (set/upsert)
|
|
429
|
+
if (projectVarsMatch && req.method === 'PUT') {
|
|
430
|
+
let body = '';
|
|
431
|
+
let oversize = false;
|
|
432
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_BODY) { oversize = true; req.destroy(); } });
|
|
433
|
+
req.on('end', () => {
|
|
434
|
+
if (oversize) { jsonResponse(res, { error: 'Payload too large' }, 413); return; }
|
|
435
|
+
try {
|
|
436
|
+
const projectId = parseInt(projectVarsMatch[1], 10);
|
|
437
|
+
const { scope, key, value } = JSON.parse(body);
|
|
438
|
+
if (!key || value === undefined) { jsonResponse(res, { error: 'Missing key or value' }, 400); return; }
|
|
439
|
+
dbSetVariable(projectId, scope || 'project', key, value);
|
|
440
|
+
jsonResponse(res, { ok: true });
|
|
441
|
+
} catch (error) {
|
|
442
|
+
jsonResponse(res, { error: error.message }, 500);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// API: DB — project variables (delete)
|
|
449
|
+
const varDeleteMatch = pathname.match(/^\/api\/db\/projects\/(\d+)\/variables\/([^/]+)\/([^/]+)$/);
|
|
450
|
+
if (varDeleteMatch && req.method === 'DELETE') {
|
|
451
|
+
try {
|
|
452
|
+
const projectId = parseInt(varDeleteMatch[1], 10);
|
|
453
|
+
const scope = decodeURIComponent(varDeleteMatch[2]);
|
|
454
|
+
const key = decodeURIComponent(varDeleteMatch[3]);
|
|
455
|
+
const deleted = dbDeleteVariable(projectId, scope, key);
|
|
456
|
+
if (deleted) {
|
|
457
|
+
jsonResponse(res, { ok: true });
|
|
458
|
+
} else {
|
|
459
|
+
jsonResponse(res, { error: 'Variable not found' }, 404);
|
|
460
|
+
}
|
|
461
|
+
} catch (error) {
|
|
462
|
+
jsonResponse(res, { error: error.message }, 500);
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
337
467
|
// API: DB — project learnings (summary or specific category)
|
|
338
468
|
const learningsMatch = pathname.match(/^\/api\/db\/projects\/(\d+)\/learnings(?:\/(\w+))?$/);
|
|
339
469
|
if (learningsMatch) {
|
|
@@ -593,11 +723,12 @@ export async function startDashboard(config) {
|
|
|
593
723
|
},
|
|
594
724
|
});
|
|
595
725
|
|
|
596
|
-
// Pool status polling
|
|
726
|
+
// Pool status polling (aggregated across all pools)
|
|
727
|
+
const poolUrls = getPoolUrls(config);
|
|
597
728
|
const pollInterval = setInterval(async () => {
|
|
598
729
|
try {
|
|
599
|
-
const
|
|
600
|
-
wss.broadcast(JSON.stringify({ event: 'pool:status', data:
|
|
730
|
+
const aggregated = await getAggregatedPoolStatus(poolUrls);
|
|
731
|
+
wss.broadcast(JSON.stringify({ event: 'pool:status', data: aggregated }));
|
|
601
732
|
} catch { /* */ }
|
|
602
733
|
}, 5000);
|
|
603
734
|
|
|
@@ -624,7 +755,8 @@ export async function startDashboard(config) {
|
|
|
624
755
|
const projectCwd = dbGetProjectCwd(params.projectId);
|
|
625
756
|
if (!projectCwd) throw new Error('Project not found');
|
|
626
757
|
runConfig = await loadConfig({}, projectCwd);
|
|
627
|
-
// Inherit pool
|
|
758
|
+
// Inherit pool URLs from dashboard config (pool is shared)
|
|
759
|
+
runConfig._poolUrls = getPoolUrls(config);
|
|
628
760
|
runConfig.poolUrl = config.poolUrl;
|
|
629
761
|
} else {
|
|
630
762
|
runConfig = { ...config };
|
|
@@ -647,12 +779,12 @@ export async function startDashboard(config) {
|
|
|
647
779
|
({ tests, hooks } = loadAllSuites(runConfig.testsDir, runConfig.modulesDir, runConfig.exclude));
|
|
648
780
|
}
|
|
649
781
|
|
|
650
|
-
await
|
|
782
|
+
await waitForAnyPool(getPoolUrls(runConfig));
|
|
651
783
|
const results = await runTestsParallel(tests, runConfig, hooks || {});
|
|
652
784
|
const report = generateReport(results);
|
|
653
785
|
const suiteName = params.suite || null;
|
|
654
786
|
saveReport(report, runConfig.screenshotsDir, runConfig);
|
|
655
|
-
persistRun(report, runConfig, suiteName);
|
|
787
|
+
await persistRun(report, runConfig, suiteName);
|
|
656
788
|
latestReport = report;
|
|
657
789
|
currentRun = { running: false };
|
|
658
790
|
} catch (error) {
|
|
@@ -662,7 +794,7 @@ export async function startDashboard(config) {
|
|
|
662
794
|
}
|
|
663
795
|
|
|
664
796
|
return new Promise((resolve, reject) => {
|
|
665
|
-
const host = config.dashboardHost || '
|
|
797
|
+
const host = config.dashboardHost || process.env.DASHBOARD_HOST || '0.0.0.0';
|
|
666
798
|
|
|
667
799
|
server.on('error', (err) => {
|
|
668
800
|
if (err.code === 'EADDRINUSE') {
|
package/src/db.js
CHANGED
|
@@ -231,6 +231,30 @@ function migrate(db) {
|
|
|
231
231
|
);
|
|
232
232
|
`);
|
|
233
233
|
|
|
234
|
+
// ── Variables table ──────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
db.exec(`
|
|
237
|
+
CREATE TABLE IF NOT EXISTS variables (
|
|
238
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
239
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
240
|
+
scope TEXT NOT NULL DEFAULT 'project',
|
|
241
|
+
key TEXT NOT NULL,
|
|
242
|
+
value TEXT NOT NULL,
|
|
243
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
244
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
245
|
+
UNIQUE(project_id, scope, key)
|
|
246
|
+
);
|
|
247
|
+
CREATE INDEX IF NOT EXISTS idx_vars_project ON variables(project_id);
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_vars_scope ON variables(project_id, scope);
|
|
249
|
+
`);
|
|
250
|
+
|
|
251
|
+
// Add pool_url column for multi-pool tracking
|
|
252
|
+
try {
|
|
253
|
+
db.prepare('SELECT pool_url FROM test_results LIMIT 0').run();
|
|
254
|
+
} catch {
|
|
255
|
+
db.exec('ALTER TABLE test_results ADD COLUMN pool_url TEXT');
|
|
256
|
+
}
|
|
257
|
+
|
|
234
258
|
// Migrations: add metadata columns to screenshot_hashes
|
|
235
259
|
const ssColumns = db.pragma('table_info(screenshot_hashes)').map(c => c.name);
|
|
236
260
|
if (!ssColumns.includes('test_name')) {
|
|
@@ -322,8 +346,8 @@ export function saveRun(projectId, report, runId, suiteName, triggeredBy) {
|
|
|
322
346
|
`);
|
|
323
347
|
|
|
324
348
|
const insertTest = d.prepare(`
|
|
325
|
-
INSERT INTO test_results (run_id, name, success, error, start_time, end_time, duration_ms, attempt, max_attempts, error_screenshot, console_logs, network_errors, screenshots, network_logs, actions_json)
|
|
326
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
349
|
+
INSERT INTO test_results (run_id, name, success, error, start_time, end_time, duration_ms, attempt, max_attempts, error_screenshot, console_logs, network_errors, screenshots, network_logs, actions_json, pool_url)
|
|
350
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
327
351
|
`);
|
|
328
352
|
|
|
329
353
|
const insertHash = d.prepare('INSERT OR IGNORE INTO screenshot_hashes (hash, file_path, project_id, run_id, test_name, step_index, page_url, screenshot_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
|
@@ -382,6 +406,7 @@ export function saveRun(projectId, report, runId, suiteName, triggeredBy) {
|
|
|
382
406
|
screenshots.length ? JSON.stringify(screenshots) : null,
|
|
383
407
|
r.networkLogs?.length ? JSON.stringify(r.networkLogs) : null,
|
|
384
408
|
actionsCondensed.length ? JSON.stringify(actionsCondensed) : null,
|
|
409
|
+
r.poolUrl || null,
|
|
385
410
|
);
|
|
386
411
|
|
|
387
412
|
// Register screenshot hashes with metadata
|
|
@@ -397,6 +422,9 @@ export function saveRun(projectId, report, runId, suiteName, triggeredBy) {
|
|
|
397
422
|
if (r.verificationScreenshot) {
|
|
398
423
|
insertHash.run(computeScreenshotHash(r.verificationScreenshot), r.verificationScreenshot, projectId, runDbId, r.name, null, null, 'verification');
|
|
399
424
|
}
|
|
425
|
+
if (r.baselineScreenshot) {
|
|
426
|
+
insertHash.run(computeScreenshotHash(r.baselineScreenshot), r.baselineScreenshot, projectId, runDbId, r.name, null, null, 'baseline');
|
|
427
|
+
}
|
|
400
428
|
}
|
|
401
429
|
|
|
402
430
|
return runDbId;
|
|
@@ -405,6 +433,33 @@ export function saveRun(projectId, report, runId, suiteName, triggeredBy) {
|
|
|
405
433
|
return tx();
|
|
406
434
|
}
|
|
407
435
|
|
|
436
|
+
/** Save a run from sync (remote instance). Returns the run's DB id. */
|
|
437
|
+
export function persistRunFromSync({ projectId, runId, total, passed, failed, passRate, duration, generatedAt, suiteName, triggeredBy, syncInstanceId, syncOrigin }) {
|
|
438
|
+
const d = getDb();
|
|
439
|
+
|
|
440
|
+
const stmt = d.prepare(`
|
|
441
|
+
INSERT INTO runs (project_id, run_id, total, passed, failed, pass_rate, duration, generated_at, suite_name, triggered_by, sync_instance_id, sync_origin, synced_at)
|
|
442
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
443
|
+
`);
|
|
444
|
+
|
|
445
|
+
const result = stmt.run(
|
|
446
|
+
projectId,
|
|
447
|
+
runId,
|
|
448
|
+
total,
|
|
449
|
+
passed,
|
|
450
|
+
failed,
|
|
451
|
+
passRate,
|
|
452
|
+
duration,
|
|
453
|
+
generatedAt,
|
|
454
|
+
suiteName || null,
|
|
455
|
+
triggeredBy || null,
|
|
456
|
+
syncInstanceId || null,
|
|
457
|
+
syncOrigin || 'remote'
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
return result.lastInsertRowid;
|
|
461
|
+
}
|
|
462
|
+
|
|
408
463
|
/** List all projects with aggregated stats. */
|
|
409
464
|
export function listProjects() {
|
|
410
465
|
const d = getDb();
|
|
@@ -487,6 +542,7 @@ export function getRunDetail(runDbId) {
|
|
|
487
542
|
networkLogs: t.network_logs ? JSON.parse(t.network_logs) : [],
|
|
488
543
|
actions: t.actions_json ? JSON.parse(t.actions_json) : [],
|
|
489
544
|
screenshotHashes,
|
|
545
|
+
poolUrl: t.pool_url || null,
|
|
490
546
|
};
|
|
491
547
|
}),
|
|
492
548
|
};
|
|
@@ -572,7 +628,79 @@ export function getNetworkLogs(runDbId, filters = {}) {
|
|
|
572
628
|
return results;
|
|
573
629
|
}
|
|
574
630
|
|
|
631
|
+
// ── Variables CRUD ────────────────────────────────────────────────────────────
|
|
632
|
+
|
|
633
|
+
/** Upsert a variable. Scope is 'project' or a suite name. */
|
|
634
|
+
export function setVariable(projectId, scope, key, value) {
|
|
635
|
+
const d = getDb();
|
|
636
|
+
d.prepare(`
|
|
637
|
+
INSERT INTO variables (project_id, scope, key, value, updated_at)
|
|
638
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
639
|
+
ON CONFLICT(project_id, scope, key)
|
|
640
|
+
DO UPDATE SET value = excluded.value, updated_at = datetime('now')
|
|
641
|
+
`).run(projectId, scope, key, value);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Get variables for a specific scope. Returns { key: value } map. */
|
|
645
|
+
export function getVariables(projectId, scope) {
|
|
646
|
+
const d = getDb();
|
|
647
|
+
const rows = d.prepare('SELECT key, value FROM variables WHERE project_id = ? AND scope = ?').all(projectId, scope);
|
|
648
|
+
const map = {};
|
|
649
|
+
for (const r of rows) map[r.key] = r.value;
|
|
650
|
+
return map;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Delete a variable. Returns true if deleted. */
|
|
654
|
+
export function deleteVariable(projectId, scope, key) {
|
|
655
|
+
const d = getDb();
|
|
656
|
+
const info = d.prepare('DELETE FROM variables WHERE project_id = ? AND scope = ? AND key = ?').run(projectId, scope, key);
|
|
657
|
+
return info.changes > 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** List all variables for a project, grouped by scope. Returns { scope: { key: value } }. */
|
|
661
|
+
export function listVariables(projectId) {
|
|
662
|
+
const d = getDb();
|
|
663
|
+
const rows = d.prepare('SELECT scope, key, value FROM variables WHERE project_id = ? ORDER BY scope, key').all(projectId);
|
|
664
|
+
const grouped = {};
|
|
665
|
+
for (const r of rows) {
|
|
666
|
+
if (!grouped[r.scope]) grouped[r.scope] = {};
|
|
667
|
+
grouped[r.scope][r.key] = r.value;
|
|
668
|
+
}
|
|
669
|
+
return grouped;
|
|
670
|
+
}
|
|
671
|
+
|
|
575
672
|
/** Close the database connection. */
|
|
673
|
+
/** Projects with sparkline data (last N run pass rates, oldest→newest). */
|
|
674
|
+
export function listProjectsWithSparklines(sparklineSize = 20) {
|
|
675
|
+
const d = getDb();
|
|
676
|
+
const projects = d.prepare(`
|
|
677
|
+
SELECT
|
|
678
|
+
p.id, p.cwd, p.name, p.screenshots_dir, p.tests_dir, p.created_at, p.updated_at,
|
|
679
|
+
COUNT(r.id) AS runCount,
|
|
680
|
+
MAX(r.generated_at) AS lastRunAt,
|
|
681
|
+
(SELECT r2.pass_rate FROM runs r2 WHERE r2.project_id = p.id ORDER BY r2.generated_at DESC LIMIT 1) AS lastPassRate
|
|
682
|
+
FROM projects p
|
|
683
|
+
LEFT JOIN runs r ON r.project_id = p.id
|
|
684
|
+
GROUP BY p.id
|
|
685
|
+
ORDER BY p.updated_at DESC
|
|
686
|
+
`).all();
|
|
687
|
+
|
|
688
|
+
const sparkStmt = d.prepare(`
|
|
689
|
+
SELECT CAST(pass_rate AS REAL) AS rate
|
|
690
|
+
FROM runs
|
|
691
|
+
WHERE project_id = ?
|
|
692
|
+
ORDER BY generated_at DESC
|
|
693
|
+
LIMIT ?
|
|
694
|
+
`);
|
|
695
|
+
|
|
696
|
+
return projects.map(p => {
|
|
697
|
+
const rawRates = sparkStmt.all(p.id, sparklineSize).map(r => r.rate);
|
|
698
|
+
// Reverse to oldest→newest
|
|
699
|
+
rawRates.reverse();
|
|
700
|
+
return { ...p, sparkline: rawRates };
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
576
704
|
export function closeDb() {
|
|
577
705
|
if (db) {
|
|
578
706
|
db.close();
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
export { loadConfig } from './config.js';
|
|
11
11
|
export { waitForPool, connectToPool, startPool, stopPool, restartPool, getPoolStatus } from './pool.js';
|
|
12
|
+
export { getPoolUrls, getAllPoolStatuses, getAggregatedPoolStatus, waitForAnyPool, selectPool, selectAndConnect } from './pool-manager.js';
|
|
12
13
|
export { executeAction } from './actions.js';
|
|
13
14
|
export { runTest, runTestsParallel, loadTestFile, loadTestSuite, loadAllSuites, listSuites } from './runner.js';
|
|
14
15
|
export { generateReport, generateJUnitXML, saveReport, printReport, saveHistory, loadHistory, loadHistoryRun } from './reporter.js';
|
|
@@ -18,13 +19,13 @@ export { buildPrompt, generateTests, hasApiKey } from './ai-generate.js';
|
|
|
18
19
|
export { verifyIssue } from './verify.js';
|
|
19
20
|
export { resolveTestData, loadModuleRegistry, listModules } from './module-resolver.js';
|
|
20
21
|
export { learnFromRun, categorizeError } from './learner.js';
|
|
21
|
-
export { getLearningsSummary, getFlakySummary, getSelectorStability, getPageHealth, getApiHealth, getErrorPatterns, getTestTrends, getRunInsights } from './learner-sqlite.js';
|
|
22
|
+
export { getLearningsSummary, getFlakySummary, getSelectorStability, getPageHealth, getApiHealth, getErrorPatterns, getTestTrends, getRunInsights, getTestCreationContext, generateImprovements } from './learner-sqlite.js';
|
|
22
23
|
export { generateLearningsMarkdown } from './learner-markdown.js';
|
|
23
24
|
export { writeToGraph, queryGraph, closeNeo4j } from './learner-neo4j.js';
|
|
24
25
|
export { startNeo4j, stopNeo4j, getNeo4jStatus } from './neo4j-pool.js';
|
|
25
26
|
|
|
26
27
|
import { loadConfig } from './config.js';
|
|
27
|
-
import {
|
|
28
|
+
import { waitForAnyPool, getPoolUrls } from './pool-manager.js';
|
|
28
29
|
import { runTestsParallel, loadTestFile, loadTestSuite, loadAllSuites } from './runner.js';
|
|
29
30
|
import { generateReport, saveReport, printReport } from './reporter.js';
|
|
30
31
|
|
|
@@ -41,7 +42,7 @@ export async function createRunner(userConfig = {}) {
|
|
|
41
42
|
|
|
42
43
|
/** Runs all test suites from the tests directory */
|
|
43
44
|
async runAll() {
|
|
44
|
-
await
|
|
45
|
+
await waitForAnyPool(getPoolUrls(config));
|
|
45
46
|
const { tests, hooks } = loadAllSuites(config.testsDir, config.modulesDir, config.exclude);
|
|
46
47
|
const results = await runTestsParallel(tests, config, hooks);
|
|
47
48
|
const report = generateReport(results);
|
|
@@ -52,7 +53,7 @@ export async function createRunner(userConfig = {}) {
|
|
|
52
53
|
|
|
53
54
|
/** Runs a single suite by name */
|
|
54
55
|
async runSuite(name) {
|
|
55
|
-
await
|
|
56
|
+
await waitForAnyPool(getPoolUrls(config));
|
|
56
57
|
const { tests, hooks } = loadTestSuite(name, config.testsDir, config.modulesDir);
|
|
57
58
|
const results = await runTestsParallel(tests, config, hooks);
|
|
58
59
|
const report = generateReport(results);
|
|
@@ -63,7 +64,7 @@ export async function createRunner(userConfig = {}) {
|
|
|
63
64
|
|
|
64
65
|
/** Runs an array of test objects */
|
|
65
66
|
async runTests(tests) {
|
|
66
|
-
await
|
|
67
|
+
await waitForAnyPool(getPoolUrls(config));
|
|
67
68
|
const results = await runTestsParallel(tests, config);
|
|
68
69
|
const report = generateReport(results);
|
|
69
70
|
saveReport(report, config.screenshotsDir, config);
|
|
@@ -73,7 +74,7 @@ export async function createRunner(userConfig = {}) {
|
|
|
73
74
|
|
|
74
75
|
/** Runs tests from a JSON file path */
|
|
75
76
|
async runFile(filePath) {
|
|
76
|
-
await
|
|
77
|
+
await waitForAnyPool(getPoolUrls(config));
|
|
77
78
|
const { tests, hooks } = loadTestFile(filePath, config.modulesDir);
|
|
78
79
|
const results = await runTestsParallel(tests, config, hooks);
|
|
79
80
|
const report = generateReport(results);
|