@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.
Files changed (56) hide show
  1. package/.claude-plugin/marketplace.json +37 -6
  2. package/.claude-plugin/plugin.json +17 -3
  3. package/LICENSE +190 -0
  4. package/README.md +151 -527
  5. package/agents/test-creator.md +4 -2
  6. package/agents/test-improver.md +5 -3
  7. package/bin/cli.js +84 -20
  8. package/commands/capture.md +45 -0
  9. package/package.json +3 -2
  10. package/skills/e2e-testing/SKILL.md +3 -2
  11. package/skills/e2e-testing/references/action-types.md +22 -4
  12. package/skills/e2e-testing/references/test-json-format.md +23 -0
  13. package/src/actions.js +321 -14
  14. package/src/ai-generate.js +81 -0
  15. package/src/app-pool.js +339 -0
  16. package/src/config.js +131 -7
  17. package/src/dashboard.js +209 -11
  18. package/src/db.js +74 -7
  19. package/src/index.js +6 -4
  20. package/src/learner-sqlite.js +154 -0
  21. package/src/learner.js +70 -3
  22. package/src/mcp-tools.js +259 -34
  23. package/src/module-analysis.js +247 -0
  24. package/src/module-resolver.js +35 -2
  25. package/src/narrate.js +42 -1
  26. package/src/pool-manager.js +68 -17
  27. package/src/pool.js +464 -37
  28. package/src/reporter.js +4 -1
  29. package/src/runner.js +410 -63
  30. package/src/visual-diff.js +515 -0
  31. package/src/websocket.js +14 -3
  32. package/src/wizard.js +184 -0
  33. package/templates/build-dashboard.js +3 -0
  34. package/templates/dashboard/js/api.js +62 -3
  35. package/templates/dashboard/js/init.js +46 -0
  36. package/templates/dashboard/js/keyboard.js +8 -7
  37. package/templates/dashboard/js/quicksearch.js +277 -0
  38. package/templates/dashboard/js/state.js +61 -7
  39. package/templates/dashboard/js/toast.js +1 -1
  40. package/templates/dashboard/js/utils.js +20 -0
  41. package/templates/dashboard/js/view-live.js +240 -9
  42. package/templates/dashboard/js/view-runs.js +540 -94
  43. package/templates/dashboard/js/view-tests.js +157 -16
  44. package/templates/dashboard/js/view-tools.js +234 -0
  45. package/templates/dashboard/js/view-watch.js +2 -2
  46. package/templates/dashboard/js/websocket.js +36 -0
  47. package/templates/dashboard/styles/base.css +489 -53
  48. package/templates/dashboard/styles/components.css +719 -77
  49. package/templates/dashboard/styles/view-live.css +463 -59
  50. package/templates/dashboard/styles/view-runs.css +793 -155
  51. package/templates/dashboard/styles/view-tests.css +440 -77
  52. package/templates/dashboard/styles/view-tools.css +206 -0
  53. package/templates/dashboard/styles/view-watch.css +198 -41
  54. package/templates/dashboard/template.html +369 -56
  55. package/templates/dashboard.html +5375 -901
  56. package/templates/docker-compose-lightpanda.yml +7 -0
@@ -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 `focus_autocomplete` + `click_option` (MUI)
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
@@ -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 proper waits on selectors or text
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 Scaffold e2e/ in the current project
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 Scaffold e2e/ in the current project
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 pressure = await waitForAnyPool(poolUrls);
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
- // Copy config template
469
+ // Write generated config
409
470
  const configDest = path.join(cwd, 'e2e.config.js');
410
471
  if (!fs.existsSync(configDest)) {
411
- fs.copyFileSync(path.join(templatesDir, 'e2e.config.js'), configDest);
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
- const testDest = path.join(cwd, 'e2e', 'tests', 'sample.json');
419
- if (!fs.existsSync(testDest)) {
420
- fs.copyFileSync(path.join(templatesDir, 'sample-test.json'), testDest);
421
- log('📄', 'Created e2e/tests/sample.json');
422
- } else {
423
- log('⏭️', 'e2e/tests/sample.json already exists, skipping');
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. Edit ${C.cyan}e2e/tests/sample.json${C.reset} with your tests
459
- 3. Start the pool: ${C.cyan}e2e-runner pool start${C.reset}
460
- 4. Run your tests: ${C.cyan}e2e-runner run --all${C.reset}
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
- await waitForAnyPool(capturePoolUrls);
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.0",
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. Dispatches `input` + `change` events so React state updates. Supports `<input>` and `<textarea>`. |
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) | Wait for selector, text on page, or fixed delay. |
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: