@matware/e2e-runner 1.0.1 → 1.0.3

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 CHANGED
@@ -318,13 +318,22 @@ import {
318
318
 
319
319
  ## Claude Code Integration (MCP)
320
320
 
321
- The package includes a built-in [MCP server](https://modelcontextprotocol.io/) that gives Claude Code native access to the test runner. Install once and it's available in every project:
321
+ The package includes a built-in [MCP server](https://modelcontextprotocol.io/) that gives Claude Code native access to the test runner. Install once and it's available in every project.
322
+
323
+ **Via npm** (requires Node.js):
322
324
 
323
325
  ```bash
324
326
  claude mcp add --transport stdio --scope user e2e-runner \
325
327
  -- npx -y -p @matware/e2e-runner e2e-runner-mcp
326
328
  ```
327
329
 
330
+ **Via Docker** (no Node.js required):
331
+
332
+ ```bash
333
+ claude mcp add --transport stdio --scope user e2e-runner \
334
+ -- docker run -i --rm fastslack/e2e-runner-mcp
335
+ ```
336
+
328
337
  ### MCP Tools
329
338
 
330
339
  | Tool | Description |
@@ -336,6 +345,8 @@ claude mcp add --transport stdio --scope user e2e-runner \
336
345
  | `e2e_pool_start` | Start the Chrome pool Docker container |
337
346
  | `e2e_pool_stop` | Stop the Chrome pool |
338
347
 
348
+ All tools accept an optional `cwd` parameter (absolute path to the project root). Claude Code passes its current working directory so the MCP server resolves `e2e/tests/`, `e2e.config.js`, and `.e2e-pool/` relative to the correct project — even when switching between multiple projects in the same session.
349
+
339
350
  Once installed, Claude Code can run tests, analyze failures, create new test files, and manage the Chrome pool as part of its normal workflow. Just ask:
340
351
 
341
352
  > "Run all E2E tests"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matware/e2e-runner",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "mcpName": "io.github.fastslack/e2e-runner",
5
5
  "description": "E2E test runner using Chrome Pool (browserless/chrome) with parallel execution",
6
6
  "type": "module",
package/src/config.js CHANGED
@@ -68,8 +68,8 @@ async function loadConfigFile(cwd) {
68
68
  return {};
69
69
  }
70
70
 
71
- export async function loadConfig(cliArgs = {}) {
72
- const cwd = process.cwd();
71
+ export async function loadConfig(cliArgs = {}, cwd = null) {
72
+ cwd = cwd || process.cwd();
73
73
  const fileConfig = await loadConfigFile(cwd);
74
74
  const envConfig = loadEnvVars();
75
75
 
package/src/mcp-server.js CHANGED
@@ -61,6 +61,10 @@ const TOOLS = [
61
61
  type: 'number',
62
62
  description: 'Number of retries for failed tests',
63
63
  },
64
+ cwd: {
65
+ type: 'string',
66
+ description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
67
+ },
64
68
  },
65
69
  },
66
70
  },
@@ -70,7 +74,12 @@ const TOOLS = [
70
74
  'List all available E2E test suites with their test names and counts.',
71
75
  inputSchema: {
72
76
  type: 'object',
73
- properties: {},
77
+ properties: {
78
+ cwd: {
79
+ type: 'string',
80
+ description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
81
+ },
82
+ },
74
83
  },
75
84
  },
76
85
  {
@@ -122,6 +131,10 @@ const TOOLS = [
122
131
  afterEach: { type: 'array', items: { type: 'object' } },
123
132
  },
124
133
  },
134
+ cwd: {
135
+ type: 'string',
136
+ description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
137
+ },
125
138
  },
126
139
  required: ['name', 'tests'],
127
140
  },
@@ -132,7 +145,12 @@ const TOOLS = [
132
145
  'Get the status of the Chrome pool (browserless/chrome). Shows availability, running sessions, capacity, and queued requests.',
133
146
  inputSchema: {
134
147
  type: 'object',
135
- properties: {},
148
+ properties: {
149
+ cwd: {
150
+ type: 'string',
151
+ description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
152
+ },
153
+ },
136
154
  },
137
155
  },
138
156
  {
@@ -150,6 +168,10 @@ const TOOLS = [
150
168
  type: 'number',
151
169
  description: 'Max concurrent Chrome sessions (default 10)',
152
170
  },
171
+ cwd: {
172
+ type: 'string',
173
+ description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
174
+ },
153
175
  },
154
176
  },
155
177
  },
@@ -158,7 +180,12 @@ const TOOLS = [
158
180
  description: 'Stop the Chrome pool Docker container.',
159
181
  inputSchema: {
160
182
  type: 'object',
161
- properties: {},
183
+ properties: {
184
+ cwd: {
185
+ type: 'string',
186
+ description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
187
+ },
188
+ },
162
189
  },
163
190
  },
164
191
  ];
@@ -171,7 +198,7 @@ async function handleRun(args) {
171
198
  if (args.baseUrl) configOverrides.baseUrl = args.baseUrl;
172
199
  if (args.retries !== undefined) configOverrides.retries = args.retries;
173
200
 
174
- const config = await loadConfig(configOverrides);
201
+ const config = await loadConfig(configOverrides, args.cwd);
175
202
 
176
203
  await waitForPool(config.poolUrl);
177
204
 
@@ -182,7 +209,8 @@ async function handleRun(args) {
182
209
  } else if (args.suite) {
183
210
  ({ tests, hooks } = loadTestSuite(args.suite, config.testsDir));
184
211
  } else if (args.file) {
185
- const filePath = path.isAbsolute(args.file) ? args.file : path.resolve(args.file);
212
+ const cwd = args.cwd || process.cwd();
213
+ const filePath = path.isAbsolute(args.file) ? args.file : path.resolve(cwd, args.file);
186
214
  ({ tests, hooks } = loadTestFile(filePath));
187
215
  } else {
188
216
  return errorResult('Provide one of: all (true), suite (name), or file (path)');
@@ -213,14 +241,23 @@ async function handleRun(args) {
213
241
  reportPath: path.join(config.screenshotsDir, 'report.json'),
214
242
  };
215
243
 
244
+ const consoleErrors = report.results
245
+ .filter(r => r.consoleLogs?.some(l => l.type === 'error' || l.type === 'warning'))
246
+ .map(r => ({ name: r.name, logs: r.consoleLogs.filter(l => l.type === 'error' || l.type === 'warning') }));
247
+ const networkErrors = report.results
248
+ .filter(r => r.networkErrors?.length > 0)
249
+ .map(r => ({ name: r.name, errors: r.networkErrors }));
250
+
216
251
  if (flaky.length > 0) summary.flaky = flaky;
217
252
  if (failures.length > 0) summary.failures = failures;
253
+ if (consoleErrors.length > 0) summary.consoleErrors = consoleErrors;
254
+ if (networkErrors.length > 0) summary.networkErrors = networkErrors;
218
255
 
219
256
  return textResult(JSON.stringify(summary, null, 2));
220
257
  }
221
258
 
222
- async function handleList() {
223
- const config = await loadConfig({});
259
+ async function handleList(args) {
260
+ const config = await loadConfig({}, args.cwd);
224
261
  const suites = listSuites(config.testsDir);
225
262
 
226
263
  if (suites.length === 0) {
@@ -235,7 +272,7 @@ async function handleList() {
235
272
  }
236
273
 
237
274
  async function handleCreateTest(args) {
238
- const config = await loadConfig({});
275
+ const config = await loadConfig({}, args.cwd);
239
276
 
240
277
  if (!fs.existsSync(config.testsDir)) {
241
278
  fs.mkdirSync(config.testsDir, { recursive: true });
@@ -259,8 +296,8 @@ async function handleCreateTest(args) {
259
296
  return textResult(`Created test file: ${filePath}\n\n${args.tests.length} test(s) defined.`);
260
297
  }
261
298
 
262
- async function handlePoolStatus() {
263
- const config = await loadConfig({});
299
+ async function handlePoolStatus(args) {
300
+ const config = await loadConfig({}, args.cwd);
264
301
  const status = await getPoolStatus(config.poolUrl);
265
302
 
266
303
  const lines = [
@@ -282,14 +319,14 @@ async function handlePoolStart(args) {
282
319
  if (args.port) overrides.poolPort = args.port;
283
320
  if (args.maxSessions) overrides.maxSessions = args.maxSessions;
284
321
 
285
- const config = await loadConfig(overrides);
286
- startPool(config);
322
+ const config = await loadConfig(overrides, args.cwd);
323
+ startPool(config, args.cwd);
287
324
  return textResult(`Chrome pool started on port ${config.poolPort}`);
288
325
  }
289
326
 
290
- async function handlePoolStop() {
291
- const config = await loadConfig({});
292
- stopPool(config);
327
+ async function handlePoolStop(args) {
328
+ const config = await loadConfig({}, args.cwd);
329
+ stopPool(config, args.cwd);
293
330
  return textResult('Chrome pool stopped');
294
331
  }
295
332
 
@@ -323,15 +360,15 @@ export async function startMcpServer() {
323
360
  case 'e2e_run':
324
361
  return await handleRun(args);
325
362
  case 'e2e_list':
326
- return await handleList();
363
+ return await handleList(args);
327
364
  case 'e2e_create_test':
328
365
  return await handleCreateTest(args);
329
366
  case 'e2e_pool_status':
330
- return await handlePoolStatus();
367
+ return await handlePoolStatus(args);
331
368
  case 'e2e_pool_start':
332
369
  return await handlePoolStart(args);
333
370
  case 'e2e_pool_stop':
334
- return await handlePoolStop();
371
+ return await handlePoolStop(args);
335
372
  default:
336
373
  return errorResult(`Unknown tool: ${name}`);
337
374
  }
package/src/pool.js CHANGED
@@ -61,8 +61,8 @@ export async function connectToPool(poolUrl, retries = 3, delay = 2000) {
61
61
  }
62
62
 
63
63
  /** Generates docker-compose.yml and starts the pool */
64
- export function startPool(config) {
65
- const cwd = process.cwd();
64
+ export function startPool(config, cwd = null) {
65
+ cwd = cwd || process.cwd();
66
66
  const poolDir = path.join(cwd, '.e2e-pool');
67
67
 
68
68
  if (!fs.existsSync(poolDir)) {
@@ -93,8 +93,9 @@ export function startPool(config) {
93
93
  }
94
94
 
95
95
  /** Stops the pool */
96
- export function stopPool(config) {
97
- const composePath = path.join(process.cwd(), '.e2e-pool', 'docker-compose.yml');
96
+ export function stopPool(config, cwd = null) {
97
+ cwd = cwd || process.cwd();
98
+ const composePath = path.join(cwd, '.e2e-pool', 'docker-compose.yml');
98
99
  if (!fs.existsSync(composePath)) {
99
100
  log('⚠️', '.e2e-pool/docker-compose.yml not found');
100
101
  return;
@@ -106,9 +107,9 @@ export function stopPool(config) {
106
107
  }
107
108
 
108
109
  /** Restarts the pool */
109
- export function restartPool(config) {
110
- stopPool(config);
111
- startPool(config);
110
+ export function restartPool(config, cwd = null) {
111
+ stopPool(config, cwd);
112
+ startPool(config, cwd);
112
113
  }
113
114
 
114
115
  /** Gets pool status */
package/src/reporter.js CHANGED
@@ -55,6 +55,11 @@ export function generateJUnitXML(report) {
55
55
  xml += ` <system-out><![CDATA[${logs}]]></system-out>\n`;
56
56
  }
57
57
 
58
+ const netErrors = (result.networkErrors || []).map(e => `[${e.error || 'unknown'}] ${e.url}`).join('\n');
59
+ if (netErrors) {
60
+ xml += ` <system-err><![CDATA[${netErrors}]]></system-err>\n`;
61
+ }
62
+
58
63
  xml += ' </testcase>\n';
59
64
  }
60
65
 
@@ -109,6 +114,31 @@ export function printReport(report, screenshotsDir) {
109
114
  });
110
115
  }
111
116
 
117
+ const consoleIssues = report.results.filter(r =>
118
+ r.consoleLogs?.some(l => l.type === 'error' || l.type === 'warning')
119
+ );
120
+ if (consoleIssues.length > 0) {
121
+ console.log(`\n${C.yellow}${C.bold}BROWSER CONSOLE ISSUES:${C.reset}`);
122
+ consoleIssues.forEach(r => {
123
+ const logs = r.consoleLogs.filter(l => l.type === 'error' || l.type === 'warning');
124
+ console.log(` ${C.yellow}⚠${C.reset} ${r.name}:`);
125
+ logs.forEach(l => {
126
+ console.log(` ${C.dim}[${l.type}]${C.reset} ${l.text}`);
127
+ });
128
+ });
129
+ }
130
+
131
+ const networkIssues = report.results.filter(r => r.networkErrors?.length > 0);
132
+ if (networkIssues.length > 0) {
133
+ console.log(`\n${C.yellow}${C.bold}NETWORK ERRORS:${C.reset}`);
134
+ networkIssues.forEach(r => {
135
+ console.log(` ${C.yellow}⚠${C.reset} ${r.name}:`);
136
+ r.networkErrors.forEach(e => {
137
+ console.log(` ${C.dim}[${e.error || 'unknown'}]${C.reset} ${e.url}`);
138
+ });
139
+ });
140
+ }
141
+
112
142
  if (screenshotsDir) {
113
143
  console.log(`\n${C.dim}Report: ${path.join(screenshotsDir, 'report.json')}${C.reset}`);
114
144
  console.log(`${C.dim}Screenshots: ${screenshotsDir}${C.reset}\n`);
package/src/runner.js CHANGED
@@ -208,6 +208,14 @@ export async function runTestsParallel(tests, config, suiteHooks = {}) {
208
208
  const attempts = result.maxAttempts > 1 ? ` (${result.maxAttempts} attempts)` : '';
209
209
  log('❌', `${C.red}${test.name}${C.reset}: ${result.error}${attempts}`);
210
210
  }
211
+
212
+ const consoleIssues = result.consoleLogs?.filter(l => l.type === 'error' || l.type === 'warning').length || 0;
213
+ if (consoleIssues > 0) {
214
+ log('⚠️', `${C.yellow}${test.name}: ${consoleIssues} console ${consoleIssues === 1 ? 'issue' : 'issues'}${C.reset}`);
215
+ }
216
+ if (result.networkErrors?.length > 0) {
217
+ log('⚠️', `${C.yellow}${test.name}: ${result.networkErrors.length} network ${result.networkErrors.length === 1 ? 'error' : 'errors'}${C.reset}`);
218
+ }
211
219
  }
212
220
  };
213
221