@matware/e2e-runner 1.3.0 → 1.5.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 +37 -6
- package/.claude-plugin/plugin.json +17 -3
- package/LICENSE +190 -0
- package/README.md +151 -527
- package/agents/test-creator.md +4 -2
- package/agents/test-improver.md +5 -3
- package/bin/cli.js +84 -20
- package/commands/capture.md +45 -0
- package/package.json +3 -2
- package/skills/e2e-testing/SKILL.md +3 -2
- package/skills/e2e-testing/references/action-types.md +22 -4
- package/skills/e2e-testing/references/test-json-format.md +23 -0
- package/src/actions.js +321 -14
- package/src/ai-generate.js +81 -0
- package/src/app-pool.js +339 -0
- package/src/config.js +131 -7
- package/src/dashboard.js +209 -11
- package/src/db.js +74 -7
- package/src/index.js +6 -4
- package/src/learner-sqlite.js +154 -0
- package/src/learner.js +70 -3
- package/src/mcp-tools.js +259 -34
- package/src/module-analysis.js +247 -0
- package/src/module-resolver.js +35 -2
- package/src/narrate.js +42 -1
- package/src/pool-manager.js +68 -17
- package/src/pool.js +464 -37
- package/src/reporter.js +4 -1
- package/src/runner.js +410 -63
- package/src/visual-diff.js +515 -0
- package/src/websocket.js +14 -3
- package/src/wizard.js +184 -0
- package/templates/build-dashboard.js +3 -0
- package/templates/dashboard/js/api.js +62 -3
- package/templates/dashboard/js/init.js +46 -0
- package/templates/dashboard/js/keyboard.js +8 -7
- package/templates/dashboard/js/quicksearch.js +277 -0
- package/templates/dashboard/js/state.js +61 -7
- package/templates/dashboard/js/toast.js +1 -1
- package/templates/dashboard/js/utils.js +20 -0
- package/templates/dashboard/js/view-live.js +240 -9
- package/templates/dashboard/js/view-runs.js +540 -94
- package/templates/dashboard/js/view-tests.js +157 -16
- package/templates/dashboard/js/view-tools.js +234 -0
- package/templates/dashboard/js/view-watch.js +2 -2
- package/templates/dashboard/js/websocket.js +36 -0
- package/templates/dashboard/styles/base.css +489 -53
- package/templates/dashboard/styles/components.css +719 -77
- package/templates/dashboard/styles/view-live.css +463 -59
- package/templates/dashboard/styles/view-runs.css +793 -155
- package/templates/dashboard/styles/view-tests.css +440 -77
- package/templates/dashboard/styles/view-tools.css +206 -0
- package/templates/dashboard/styles/view-watch.css +198 -41
- package/templates/dashboard/template.html +369 -56
- package/templates/dashboard.html +5375 -901
- package/templates/docker-compose-lightpanda.yml +7 -0
package/agents/test-creator.md
CHANGED
|
@@ -63,11 +63,12 @@ You are a specialist in creating robust E2E tests for web applications. You expl
|
|
|
63
63
|
|
|
64
64
|
### Form Interaction
|
|
65
65
|
- Standard input → `type` (clears first)
|
|
66
|
-
- React controlled input → `type_react`
|
|
67
|
-
- Dropdown select → `select` (native) or `
|
|
66
|
+
- React controlled input → `type_react` (optional `blur`, `waitAfter`)
|
|
67
|
+
- Dropdown select → `select` (native) or `select_combobox` (MUI Autocomplete/Select — opens, optional `filter`, picks `text` in one action)
|
|
68
68
|
- Checkbox/radio → `click`
|
|
69
69
|
- Clear field → `clear`
|
|
70
70
|
- Submit → `click` on submit button or `press` Enter
|
|
71
|
+
- Confirm in a modal → `click` with `text` + `scope: "dialog"` (add `last: true` if multiple matches)
|
|
71
72
|
|
|
72
73
|
### Storage
|
|
73
74
|
- Set localStorage key → `set_storage` with `value: "key=val"`
|
|
@@ -83,6 +84,7 @@ You are a specialist in creating robust E2E tests for web applications. You expl
|
|
|
83
84
|
### Waiting
|
|
84
85
|
- Element appears → `wait` with `selector`
|
|
85
86
|
- Text appears → `wait` with `text`
|
|
87
|
+
- Element/spinner/dialog disappears → `wait` with `gone` (e.g. `{ "type": "wait", "gone": ".MuiBackdrop-root" }`)
|
|
86
88
|
- Fixed delay (last resort) → `wait` with `value` (ms)
|
|
87
89
|
|
|
88
90
|
### Assertions
|
package/agents/test-improver.md
CHANGED
|
@@ -26,7 +26,7 @@ You are a specialist in refactoring and optimizing existing E2E tests without ch
|
|
|
26
26
|
- **Duplication extraction**: Identify repeated action sequences across tests and extract them into reusable modules (`$use`)
|
|
27
27
|
- **Selector hardening**: Replace brittle selectors (nth-child, deep nesting, generated classes) with stable alternatives (`data-testid`, `id`, text-based)
|
|
28
28
|
- **Flaky test stabilization**: Add `wait` actions, `retries`, and `serial: true` based on historical failure data from the learning system
|
|
29
|
-
- **Fixed delay elimination**: Replace hardcoded `wait` with ms values with
|
|
29
|
+
- **Fixed delay elimination**: Replace hardcoded `wait` with ms values with condition waits — `wait` on a `selector`/`text` to appear, or `wait` with `gone` to wait for a spinner/backdrop/dialog to disappear (e.g. `{ "type": "wait", "gone": ".MuiBackdrop-root" }`)
|
|
30
30
|
- **Visual verification**: Add `expect` fields to tests that lack visual verification
|
|
31
31
|
- **Serial marking**: Mark tests that share mutable state as `serial: true` to prevent race conditions
|
|
32
32
|
- **Hook extraction**: Move duplicated setup/teardown actions into `beforeEach`/`beforeAll` hooks
|
|
@@ -68,9 +68,11 @@ When you find an `evaluate` action, check if it matches one of these patterns
|
|
|
68
68
|
| `el.classList.contains(cls)` | `assert_class` with `selector` + `value` |
|
|
69
69
|
| `el.hasAttribute(attr)` or `el.getAttribute(attr)` | `assert_attribute` with `selector` + `value` |
|
|
70
70
|
| `document.querySelectorAll(sel).length` | `assert_count` with `selector` + `value` |
|
|
71
|
-
| Native value setter + `dispatchEvent(new Event('input'))` | `type_react` with `selector` + `value` |
|
|
72
|
-
| `querySelectorAll('[role="option"]')...click()` | `click_option` with `text` |
|
|
71
|
+
| Native value setter + `dispatchEvent(new Event('input'))` (single input) | `type_react` with `selector` + `value` (+ `blur:true` / `waitAfter` if it blurred/slept) |
|
|
72
|
+
| `querySelectorAll('[role="option"]')...click()` (no combobox open first) | `click_option` with `text` |
|
|
73
|
+
| Open combobox (focus/click input) + optional type filter + click matching option | `select_combobox` with `selector` + `text` (+ `filter`) |
|
|
73
74
|
| `MuiAutocomplete-root...input.focus()` | `focus_autocomplete` with `text` |
|
|
75
|
+
| Find a button by text inside `[role="dialog"]`/`.MuiDialog-root` and click (often the LAST one) | `click` with `text` + `scope: "dialog"` (+ `last: true`) |
|
|
74
76
|
| `querySelectorAll('button').filter(regex)...click()` | `click_regex` with `text` + optional `selector` + `value` |
|
|
75
77
|
| `querySelectorAll('[class*="Chip"]')...click()` | `click_chip` with `text` |
|
|
76
78
|
| `localStorage.setItem(key, val)` or `sessionStorage.setItem(...)` | `set_storage` with `value: "key=val"`, `selector: "session"` for session |
|
package/bin/cli.js
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* e2e-runner issue <url> --generate Generate test file via Claude API
|
|
22
22
|
* e2e-runner issue <url> --verify Generate + run + report bug status
|
|
23
23
|
* e2e-runner issue <url> --prompt Output the AI prompt (for piping)
|
|
24
|
-
* e2e-runner init
|
|
24
|
+
* e2e-runner init Interactive wizard to scaffold e2e/
|
|
25
|
+
* e2e-runner init --yes Scaffold with defaults (no prompts)
|
|
25
26
|
* e2e-runner --help Show help
|
|
26
27
|
* e2e-runner --version Show version
|
|
27
28
|
*/
|
|
@@ -43,6 +44,7 @@ import { verifyIssue } from '../src/verify.js';
|
|
|
43
44
|
import { ensureProject, computeScreenshotHash, registerScreenshotHash } from '../src/db.js';
|
|
44
45
|
import { log, colors as C } from '../src/logger.js';
|
|
45
46
|
import { listModules } from '../src/module-resolver.js';
|
|
47
|
+
import { runInitWizard, renderConfig, getDefaultAnswers } from '../src/wizard.js';
|
|
46
48
|
import { getLearningsSummary, getFlakySummary, getSelectorStability, getPageHealth, getApiHealth, getErrorPatterns, getTestTrends } from '../src/learner-sqlite.js';
|
|
47
49
|
import { startNeo4j, stopNeo4j, getNeo4jStatus } from '../src/neo4j-pool.js';
|
|
48
50
|
import {
|
|
@@ -118,6 +120,8 @@ function parseCLIConfig() {
|
|
|
118
120
|
cliArgs.verificationStrictness = val;
|
|
119
121
|
}
|
|
120
122
|
}
|
|
123
|
+
if (getFlag('--driver')) cliArgs.cliDriverOverride = getFlag('--driver');
|
|
124
|
+
if (getFlag('--fallback-driver')) cliArgs.cliFallbackDriverOverride = getFlag('--fallback-driver');
|
|
121
125
|
return cliArgs;
|
|
122
126
|
}
|
|
123
127
|
|
|
@@ -175,7 +179,10 @@ ${C.bold}Usage:${C.reset}
|
|
|
175
179
|
e2e-runner sync push Process sync queue (agent mode)
|
|
176
180
|
e2e-runner sync pull Pull runs from hub (agent mode)
|
|
177
181
|
|
|
178
|
-
e2e-runner init
|
|
182
|
+
e2e-runner init Interactive wizard to scaffold e2e/
|
|
183
|
+
e2e-runner init --yes Scaffold with defaults (CI / non-interactive)
|
|
184
|
+
Flags: --name, --base-url, --driver,
|
|
185
|
+
--pool-port, --concurrency, --no-sample
|
|
179
186
|
|
|
180
187
|
${C.bold}Options:${C.reset}
|
|
181
188
|
--base-url <url> App base URL (default: http://host.docker.internal:3000)
|
|
@@ -199,6 +206,9 @@ ${C.bold}Options:${C.reset}
|
|
|
199
206
|
--auth-login-endpoint <url> Auto-login: POST credentials to this URL to get auth token
|
|
200
207
|
--auth-token-path <path> Dot-path to token in auth response (default: token)
|
|
201
208
|
--verification-strictness <level> Visual verification: strict, moderate (default), lenient
|
|
209
|
+
--driver <name> Force pool driver for this run: browserless, cdp, lightpanda, obscura, steel
|
|
210
|
+
(overrides per-test "driver" field; useful for A/B benchmarks)
|
|
211
|
+
--fallback-driver <name> Explicit fallback if no pool with --driver is reachable (overrides per-test "fallbackDriver")
|
|
202
212
|
|
|
203
213
|
${C.bold}Watch Options:${C.reset}
|
|
204
214
|
--interval <time> Run interval: 15m, 1h, 30s (required for schedule mode)
|
|
@@ -220,6 +230,21 @@ async function cmdRun() {
|
|
|
220
230
|
const cliArgs = parseCLIConfig();
|
|
221
231
|
const config = await loadConfig(cliArgs);
|
|
222
232
|
config.triggeredBy = 'cli';
|
|
233
|
+
|
|
234
|
+
// Validate CLI driver overrides up-front (clearer error than waiting for first test)
|
|
235
|
+
if (config.cliDriverOverride || config.cliFallbackDriverOverride) {
|
|
236
|
+
const allowed = ['browserless', 'cdp', 'lightpanda', 'obscura', 'steel'];
|
|
237
|
+
for (const [flag, val] of [['--driver', config.cliDriverOverride], ['--fallback-driver', config.cliFallbackDriverOverride]]) {
|
|
238
|
+
if (val && !allowed.includes(val)) {
|
|
239
|
+
console.error(`${C.red}Invalid value for ${flag}: "${val}". Allowed: ${allowed.join(', ')}.${C.reset}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (config.cliFallbackDriverOverride && !config.cliDriverOverride) {
|
|
244
|
+
console.error(`${C.red}--fallback-driver requires --driver.${C.reset}`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
223
248
|
let tests = [];
|
|
224
249
|
let hooks = {};
|
|
225
250
|
|
|
@@ -262,9 +287,32 @@ async function cmdRun() {
|
|
|
262
287
|
process.exit(1);
|
|
263
288
|
}
|
|
264
289
|
|
|
265
|
-
// Verify pool connectivity
|
|
290
|
+
// Verify pool connectivity — auto-start the Docker-managed pool if none is
|
|
291
|
+
// reachable, so first-time users don't need a separate `pool start` step.
|
|
266
292
|
log('🔌', `Checking Chrome Pool${poolUrls.length > 1 ? 's' : ''}...`);
|
|
267
|
-
const
|
|
293
|
+
const driverOpts = { poolDriver: config.poolDriver, maxSessions: config.maxSessions };
|
|
294
|
+
const _driver = config.poolDriver || 'auto';
|
|
295
|
+
const _dockerManaged = ['auto', 'browserless', 'lightpanda'].includes(_driver);
|
|
296
|
+
const _autoStart = config.autoStartPool !== false;
|
|
297
|
+
let pressure;
|
|
298
|
+
try {
|
|
299
|
+
pressure = await waitForAnyPool(poolUrls, 5000, driverOpts);
|
|
300
|
+
} catch {
|
|
301
|
+
if (_autoStart && _dockerManaged) {
|
|
302
|
+
log('🐳', `${C.dim}No pool detected — starting Chrome pool via Docker...${C.reset}`);
|
|
303
|
+
try {
|
|
304
|
+
startPool(config);
|
|
305
|
+
} catch (se) {
|
|
306
|
+
console.error(`${C.red}Could not auto-start the Chrome pool: ${se.message}${C.reset}`);
|
|
307
|
+
console.error(`${C.dim}Is Docker running? You can also start it manually: ${C.cyan}e2e-runner pool start${C.reset}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
pressure = await waitForAnyPool(poolUrls, 45000, driverOpts);
|
|
311
|
+
} else {
|
|
312
|
+
console.error(`${C.red}No Chrome Pool available.${C.reset} Driver "${_driver}" is not Docker-managed — start your browser endpoint, then re-run.`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
268
316
|
log('✅', `Pool ready (${pressure.running}/${pressure.maxConcurrent} sessions, queued: ${pressure.queued})`);
|
|
269
317
|
|
|
270
318
|
// Wire up live progress to dashboard if running
|
|
@@ -351,7 +399,7 @@ async function cmdPool() {
|
|
|
351
399
|
|
|
352
400
|
case 'status': {
|
|
353
401
|
const statusPoolUrls = getPoolUrls(config);
|
|
354
|
-
const aggregated = await getAggregatedPoolStatus(statusPoolUrls);
|
|
402
|
+
const aggregated = await getAggregatedPoolStatus(statusPoolUrls, { poolDriver: config.poolDriver, maxSessions: config.maxSessions });
|
|
355
403
|
console.log(`\n${C.bold}Chrome Pool Status:${C.reset}\n`);
|
|
356
404
|
|
|
357
405
|
if (statusPoolUrls.length > 1) {
|
|
@@ -387,10 +435,23 @@ async function cmdPool() {
|
|
|
387
435
|
}
|
|
388
436
|
}
|
|
389
437
|
|
|
390
|
-
function cmdInit() {
|
|
438
|
+
async function cmdInit() {
|
|
391
439
|
const cwd = process.cwd();
|
|
392
440
|
const templatesDir = path.join(__dirname, '..', 'templates');
|
|
393
441
|
|
|
442
|
+
const skipWizard = hasFlag('--yes') || hasFlag('-y') || hasFlag('--non-interactive');
|
|
443
|
+
const flagOverrides = {};
|
|
444
|
+
if (getFlag('--name') && typeof getFlag('--name') === 'string') flagOverrides.projectName = getFlag('--name');
|
|
445
|
+
if (getFlag('--base-url') && typeof getFlag('--base-url') === 'string') flagOverrides.baseUrl = getFlag('--base-url');
|
|
446
|
+
if (getFlag('--driver') && typeof getFlag('--driver') === 'string') flagOverrides.driver = getFlag('--driver');
|
|
447
|
+
if (getFlag('--pool-port') && typeof getFlag('--pool-port') === 'string') flagOverrides.poolPort = parseInt(getFlag('--pool-port'), 10);
|
|
448
|
+
if (getFlag('--concurrency') && typeof getFlag('--concurrency') === 'string') flagOverrides.concurrency = parseInt(getFlag('--concurrency'), 10);
|
|
449
|
+
if (hasFlag('--no-sample')) flagOverrides.includeSampleTest = false;
|
|
450
|
+
|
|
451
|
+
const answers = skipWizard
|
|
452
|
+
? { ...getDefaultAnswers(cwd), ...flagOverrides }
|
|
453
|
+
: await runInitWizard(cwd, flagOverrides);
|
|
454
|
+
|
|
394
455
|
// Create directory structure
|
|
395
456
|
const dirs = [
|
|
396
457
|
path.join(cwd, 'e2e', 'tests'),
|
|
@@ -405,22 +466,24 @@ function cmdInit() {
|
|
|
405
466
|
}
|
|
406
467
|
}
|
|
407
468
|
|
|
408
|
-
//
|
|
469
|
+
// Write generated config
|
|
409
470
|
const configDest = path.join(cwd, 'e2e.config.js');
|
|
410
471
|
if (!fs.existsSync(configDest)) {
|
|
411
|
-
fs.
|
|
472
|
+
fs.writeFileSync(configDest, renderConfig(answers));
|
|
412
473
|
log('📄', 'Created e2e.config.js');
|
|
413
474
|
} else {
|
|
414
475
|
log('⏭️', 'e2e.config.js already exists, skipping');
|
|
415
476
|
}
|
|
416
477
|
|
|
417
478
|
// Copy sample test
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
fs.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
479
|
+
if (answers.includeSampleTest) {
|
|
480
|
+
const testDest = path.join(cwd, 'e2e', 'tests', 'sample.json');
|
|
481
|
+
if (!fs.existsSync(testDest)) {
|
|
482
|
+
fs.copyFileSync(path.join(templatesDir, 'sample-test.json'), testDest);
|
|
483
|
+
log('📄', 'Created e2e/tests/sample.json');
|
|
484
|
+
} else {
|
|
485
|
+
log('⏭️', 'e2e/tests/sample.json already exists, skipping');
|
|
486
|
+
}
|
|
424
487
|
}
|
|
425
488
|
|
|
426
489
|
// Create .gitkeep
|
|
@@ -455,9 +518,9 @@ ${C.bold}${C.green}E2E structure created!${C.reset}
|
|
|
455
518
|
|
|
456
519
|
${C.bold}Next steps:${C.reset}
|
|
457
520
|
1. Edit ${C.cyan}e2e.config.js${C.reset} with your app URL
|
|
458
|
-
2.
|
|
459
|
-
|
|
460
|
-
|
|
521
|
+
2. Run your tests: ${C.cyan}e2e-runner run --all${C.reset} ${C.dim}(starts Chrome automatically)${C.reset}
|
|
522
|
+
|
|
523
|
+
${C.dim}That's it — the runner spins up the Chrome pool for you on first run.${C.reset}
|
|
461
524
|
`);
|
|
462
525
|
}
|
|
463
526
|
|
|
@@ -494,11 +557,12 @@ async function cmdCapture() {
|
|
|
494
557
|
|
|
495
558
|
const capturePoolUrls = getPoolUrls(config);
|
|
496
559
|
log('🔌', 'Checking Chrome Pool...');
|
|
497
|
-
|
|
560
|
+
const captureDriverOpts = { poolDriver: config.poolDriver, maxSessions: config.maxSessions };
|
|
561
|
+
await waitForAnyPool(capturePoolUrls, 30000, captureDriverOpts);
|
|
498
562
|
|
|
499
563
|
let browser;
|
|
500
564
|
try {
|
|
501
|
-
const capturePool = await selectPool(capturePoolUrls);
|
|
565
|
+
const capturePool = await selectPool(capturePoolUrls, 2000, 60000, captureDriverOpts);
|
|
502
566
|
browser = await connectToPool(capturePool);
|
|
503
567
|
const page = await browser.newPage();
|
|
504
568
|
await page.setViewport(config.viewport);
|
|
@@ -1137,7 +1201,7 @@ async function main() {
|
|
|
1137
1201
|
break;
|
|
1138
1202
|
|
|
1139
1203
|
case 'init':
|
|
1140
|
-
cmdInit();
|
|
1204
|
+
await cmdInit();
|
|
1141
1205
|
break;
|
|
1142
1206
|
|
|
1143
1207
|
default:
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Capture a screenshot of any URL with automatic authentication
|
|
3
|
+
user_invocable: true
|
|
4
|
+
allowed_tools:
|
|
5
|
+
- mcp__e2e-runner__e2e_pool_status
|
|
6
|
+
- mcp__e2e-runner__e2e_capture
|
|
7
|
+
- mcp__e2e-runner__e2e_analyze
|
|
8
|
+
- mcp__e2e-runner__e2e_screenshot
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Quick Capture
|
|
12
|
+
|
|
13
|
+
Take a screenshot of any URL in one step. Handles pool checks and authentication automatically.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. **Check pool** — Call `e2e_pool_status` to confirm the Chrome pool is running. If not available, tell the user to run `npx e2e-runner pool start` via CLI and stop.
|
|
18
|
+
|
|
19
|
+
2. **Capture** — Call `e2e_capture` with:
|
|
20
|
+
- `url`: The URL from the user's request (REQUIRED)
|
|
21
|
+
- `cwd`: The current working directory (REQUIRED — always pass this)
|
|
22
|
+
- `fullPage`: true if user says "full page", "full", "complete", or "toda la página"
|
|
23
|
+
- `selector`: CSS selector if user wants to wait for a specific element
|
|
24
|
+
- `delay`: milliseconds if user says "wait", "delay", or "espera"
|
|
25
|
+
- `waitUntil`: "domcontentloaded" if user mentions WebSocket, SSE, or real-time apps
|
|
26
|
+
- `filename`: if user specifies a name
|
|
27
|
+
|
|
28
|
+
**Authentication is automatic**: the tool reads `authToken`, `authLoginEndpoint`, and `authCredentials` from the project's `e2e.config.js`. You do NOT need to pass `authToken` unless the user explicitly provides one.
|
|
29
|
+
|
|
30
|
+
3. **Show result** — The tool returns the screenshot as an inline image. Show it to the user with the file path.
|
|
31
|
+
|
|
32
|
+
## Arguments
|
|
33
|
+
|
|
34
|
+
The user passes the URL after the command:
|
|
35
|
+
- `/e2e-runner:capture http://localhost:3000/dashboard` → capture that URL
|
|
36
|
+
- `/e2e-runner:capture http://localhost/concept-maps --full-page` → full page capture
|
|
37
|
+
- `/e2e-runner:capture http://localhost/admin --delay 3000` → wait 3s before capture
|
|
38
|
+
|
|
39
|
+
If no URL is provided, ask the user for one.
|
|
40
|
+
|
|
41
|
+
## Important
|
|
42
|
+
|
|
43
|
+
- Do NOT try to manually authenticate, fetch tokens, write test files, or use curl. The tool handles auth automatically from project config.
|
|
44
|
+
- Do NOT use the `e2e_run` tool — this is a screenshot capture, not a test run.
|
|
45
|
+
- Keep it simple: one `e2e_pool_status` call + one `e2e_capture` call. That's it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matware/e2e-runner",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"puppeteer-core": "^24.0.0"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
|
-
"build:dashboard": "node templates/build-dashboard.js"
|
|
55
|
+
"build:dashboard": "node templates/build-dashboard.js",
|
|
56
|
+
"prepublishOnly": "node templates/build-dashboard.js"
|
|
56
57
|
},
|
|
57
58
|
"engines": {
|
|
58
59
|
"node": ">=20.0.0"
|
|
@@ -72,8 +72,9 @@ Use `e2e_create_test` to write test files. Use `e2e_create_module` for reusable
|
|
|
72
72
|
### Key Action Patterns
|
|
73
73
|
|
|
74
74
|
- **Navigation**: `goto` (full page load), `navigate` (SPA-friendly, non-blocking)
|
|
75
|
-
- **Interaction**: `click` (selector or text), `type`/`fill`, `select`, `press`, `hover`, `scroll`
|
|
76
|
-
- **React/MUI**: `type_react` (controlled inputs), `click_option`, `focus_autocomplete`, `click_chip`, `click_regex`
|
|
75
|
+
- **Interaction**: `click` (selector or text; text mode also takes `scope:"dialog"`, `visible:true`, `last:true`), `type`/`fill`, `select`, `press`, `hover`, `scroll`
|
|
76
|
+
- **React/MUI**: `type_react` (controlled inputs; optional `blur`, `waitAfter`), `click_option`, `select_combobox` (open+filter+pick MUI Autocomplete/Select in one action), `focus_autocomplete`, `click_chip`, `click_regex`
|
|
77
|
+
- **Waiting**: prefer conditions over sleeps — `wait` takes `selector`/`text` (appear), `gone` (disappear, e.g. spinner/closing dialog), or `value` (fixed ms, last resort); `wait_network_idle`
|
|
77
78
|
- **Assertions**: `assert_text` (page-wide), `assert_element_text` (scoped), `assert_url`, `assert_visible`, `assert_not_visible`, `assert_count`, `assert_attribute`, `assert_class`, `assert_input_value`, `assert_matches`
|
|
78
79
|
- **Extraction**: `get_text` (non-assertion, returns element text), `screenshot`
|
|
79
80
|
- **Advanced**: `evaluate` (run JS in browser), `assert_no_network_errors`, `clear_cookies`
|
|
@@ -13,7 +13,7 @@ Complete catalog of all action types supported by @matware/e2e-runner.
|
|
|
13
13
|
|
|
14
14
|
| Action | Fields | Description |
|
|
15
15
|
|--------|--------|-------------|
|
|
16
|
-
| `click` | `selector` OR `text` | Click by CSS selector or by visible text content. Text search covers: `button, a, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="listitem"], div[class*="cursor"], span, li, td, th, label, p, h1-h6, dd, dt`. |
|
|
16
|
+
| `click` | `selector` OR `text` | Click by CSS selector or by visible text content. Text search covers: `button, a, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="listitem"], div[class*="cursor"], span, li, td, th, label, p, h1-h6, dd, dt`. Optional text-mode refinements: `scope: "dialog"` (only match inside an open `[role="dialog"]`/`.MuiDialog-root`), `visible: true` (skip hidden/zero-size matches — implied by `scope:dialog`), `last: true` (click the LAST match instead of the first). Prefer these over hand-rolled `evaluate` button-by-text scans. |
|
|
17
17
|
| `type` / `fill` | `selector`, `value` | Triple-clicks to select all, then Backspace to clear, then types with 20ms delay per character. |
|
|
18
18
|
| `select` | `selector`, `value` | Select an `<option>` value in a `<select>` element. |
|
|
19
19
|
| `clear` | `selector` | Triple-click + Backspace to clear an input field. |
|
|
@@ -25,9 +25,10 @@ Complete catalog of all action types supported by @matware/e2e-runner.
|
|
|
25
25
|
|
|
26
26
|
| Action | Fields | Description |
|
|
27
27
|
|--------|--------|-------------|
|
|
28
|
-
| `type_react` | `selector`, `value` | Types into React controlled inputs using native value setter.
|
|
28
|
+
| `type_react` | `selector`, `value`, `blur` (optional), `waitAfter` (optional ms) | Types into React controlled inputs using native value setter. Focuses, then dispatches `input` + `change` events so React state updates. Supports `<input>` and `<textarea>`. `blur: true` commits on blur (for fields that validate on blur); `waitAfter: "<ms>"` waits after (e.g. for debounced autocomplete). Prefer over inline `setNativeValue` evaluates. |
|
|
29
29
|
| `click_regex` | `text` (regex), `selector` (optional), `value` (`"last"` optional) | Click element whose textContent matches regex (case-insensitive). Default: first match. `value: "last"` for last match. `selector` scopes the search. |
|
|
30
30
|
| `click_option` | `text` | Click a `[role="option"]` element by text — for autocomplete/select dropdowns. Waits for option to appear. |
|
|
31
|
+
| `select_combobox` | `selector` (optional, default `input[role='combobox']`), `text` (option to pick), `filter` (optional typed text), `openWait`/`filterWait`/`waitAfter` (optional ms) | Open a MUI Autocomplete/Select, optionally type `filter` to narrow, then click the option matching `text` (case-insensitive substring). Falls back across `[role="option"]`, `.MuiAutocomplete-option`, `li.MuiMenuItem-root`. Replaces the verbose open-input + setNativeValue + scan-options `evaluate` pattern. |
|
|
31
32
|
| `focus_autocomplete` | `text` (label text) | Focus an autocomplete input by label. Supports MUI `.MuiAutocomplete-root` and `[role="combobox"]`. |
|
|
32
33
|
| `click_chip` | `text` | Click a chip/tag element by text. Searches `[class*="Chip"]`, `[class*="chip"]`, `[data-chip]`. |
|
|
33
34
|
|
|
@@ -75,7 +76,7 @@ Complete catalog of all action types supported by @matware/e2e-runner.
|
|
|
75
76
|
|--------|--------|-------------|
|
|
76
77
|
| `get_text` | `selector` | Returns `{ value: textContent.trim() }`. Non-assertion — never fails. |
|
|
77
78
|
| `screenshot` | `value` (filename, optional) | Captures screenshot. Filename gets timestamp suffix for uniqueness. |
|
|
78
|
-
| `wait` | `selector` OR `text` OR `value` (ms) |
|
|
79
|
+
| `wait` | `selector` OR `text` OR `gone` OR `value` (ms) | Prefer **conditions over fixed sleeps**: `{ selector }` waits for it to appear, `{ text }` waits for text to appear, **`{ gone: "<css>" }`** waits until a selector disappears/hides (spinner, closing dialog), `{ gone: true, selector|text }` is the explicit form, `{ value: "<ms>" }` is a fixed delay (last resort). Replacing `wait` sleeps with `gone`/`selector` makes suites faster and less flaky. |
|
|
79
80
|
| `wait_network_idle` | `value` (idle ms, default 500), `timeout` (max wait ms, default 30000) | Waits for all network requests to complete. Uses Puppeteer's `page.waitForNetworkIdle()`. Useful after SPA page transitions or data loading. |
|
|
80
81
|
| `evaluate` | `value` (JS code) | Run JavaScript in browser context. See **Strict Evaluate** below. |
|
|
81
82
|
| `clear_cookies` | `value` (origin, optional) | Clears cookies, localStorage, sessionStorage for origin. |
|
|
@@ -107,10 +108,27 @@ Delay between retries: `actionRetryDelay` config (default 500ms).
|
|
|
107
108
|
### React input + autocomplete flow
|
|
108
109
|
```json
|
|
109
110
|
{ "type": "focus_autocomplete", "text": "Category" },
|
|
110
|
-
{ "type": "type_react", "selector": "#category-input", "value": "Electr" },
|
|
111
|
+
{ "type": "type_react", "selector": "#category-input", "value": "Electr", "waitAfter": "400" },
|
|
111
112
|
{ "type": "click_option", "text": "Electronics" }
|
|
112
113
|
```
|
|
113
114
|
|
|
115
|
+
### MUI combobox in one action (open + filter + pick)
|
|
116
|
+
```json
|
|
117
|
+
{ "type": "select_combobox", "selector": "[data-cy='specialty'] input", "filter": "cardio", "text": "Cardiología" }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Condition waits instead of fixed sleeps (faster, less flaky)
|
|
121
|
+
```json
|
|
122
|
+
{ "type": "click", "text": "Guardar" },
|
|
123
|
+
{ "type": "wait", "gone": ".MuiBackdrop-root" },
|
|
124
|
+
{ "type": "wait", "selector": "[data-testid='saved-banner']" }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Click a button inside an open dialog (no evaluate needed)
|
|
128
|
+
```json
|
|
129
|
+
{ "type": "click", "text": "Iniciar encuentro", "scope": "dialog", "last": true }
|
|
130
|
+
```
|
|
131
|
+
|
|
114
132
|
### Regex click (last match)
|
|
115
133
|
```json
|
|
116
134
|
{ "type": "click_regex", "text": "add to cart", "selector": "button", "value": "last" }
|
|
@@ -113,6 +113,29 @@ Module definition (in `e2e/modules/auth-login.json`):
|
|
|
113
113
|
}
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
### Composing modules (nested `$use` + parameter forwarding)
|
|
117
|
+
|
|
118
|
+
A module can `$use` other modules, and **forward its own params/defaults** into the
|
|
119
|
+
nested call's `params` block. Placeholders in a nested `params` value are resolved
|
|
120
|
+
against the outer module's scope before the inner module runs:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"$module": "login-and-open",
|
|
125
|
+
"params": {
|
|
126
|
+
"patientId": { "required": true },
|
|
127
|
+
"email": { "required": false, "default": "admin@test.com" }
|
|
128
|
+
},
|
|
129
|
+
"actions": [
|
|
130
|
+
{ "$use": "auth-login", "params": { "email": "{{email}}", "password": "secret" } },
|
|
131
|
+
{ "$use": "open-patient", "params": { "id": "{{patientId}}" } }
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Cycles are detected and rejected. Action types are validated **after** all `$use`
|
|
137
|
+
references are expanded.
|
|
138
|
+
|
|
116
139
|
## Suite Naming & Ordering
|
|
117
140
|
|
|
118
141
|
Files can have numeric prefixes for execution order:
|