intellitester 0.2.34 → 0.2.40

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
@@ -12,6 +12,33 @@ npx playwright install chromium
12
12
  npx playwright install
13
13
  ```
14
14
 
15
+ ## AI Assistant Integration
16
+
17
+ IntelliTester can generate comprehensive documentation for AI assistants (Claude, GPT, etc.) to help them write effective tests.
18
+
19
+ ### Generate Guide
20
+
21
+ Use the `guide` command to create an `intellitester_guide.md` file in your project:
22
+
23
+ ```bash
24
+ intellitester guide
25
+ # Or use the alias:
26
+ intellitester init-guide
27
+ ```
28
+
29
+ This generates a detailed reference document that AI assistants can read to understand:
30
+ - All action types and their syntax
31
+ - Target selector options (testId, text, css, xpath, role, description)
32
+ - Variable interpolation and built-in generators
33
+ - Best practices for test organization
34
+ - Common testing patterns (login flows, form submissions, email verification)
35
+ - Configuration options for tests, workflows, and pipelines
36
+
37
+ The guide is particularly useful when:
38
+ - Using AI assistants to generate test files
39
+ - Onboarding team members who use AI coding tools
40
+ - Maintaining consistent test patterns across a team
41
+
15
42
  ## Editor Configuration
16
43
 
17
44
  IntelliTester provides JSON schemas for YAML configuration files to enable autocomplete and validation in VS Code and other editors.
@@ -25,7 +52,8 @@ Add this to your `.vscode/settings.json` (or workspace settings):
25
52
  "yaml.schemas": {
26
53
  "./node_modules/intellitester/schemas/intellitester.config.schema.json": "intellitester.config.yaml",
27
54
  "./node_modules/intellitester/schemas/test.schema.json": "*.test.yaml",
28
- "./node_modules/intellitester/schemas/workflow.schema.json": "*.workflow.yaml"
55
+ "./node_modules/intellitester/schemas/workflow.schema.json": "*.workflow.yaml",
56
+ "./node_modules/intellitester/schemas/pipeline.schema.json": "*.pipeline.yaml"
29
57
  }
30
58
  }
31
59
  ```
@@ -45,7 +73,8 @@ If you're working on IntelliTester itself or want to use local schemas, use thes
45
73
  "yaml.schemas": {
46
74
  "./schemas/intellitester.config.schema.json": "intellitester.config.yaml",
47
75
  "./schemas/test.schema.json": "*.test.yaml",
48
- "./schemas/workflow.schema.json": "*.workflow.yaml"
76
+ "./schemas/workflow.schema.json": "*.workflow.yaml",
77
+ "./schemas/pipeline.schema.json": "*.pipeline.yaml"
49
78
  }
50
79
  }
51
80
  ```
@@ -84,6 +113,37 @@ When execution pauses, you can:
84
113
  - Examine selectors and elements
85
114
  - Continue execution when ready
86
115
 
116
+ ## Fast-Fail Conditions (errorIf)
117
+
118
+ Use `errorIf` to fail a step immediately when a condition is met, without waiting for timeouts:
119
+
120
+ ```yaml
121
+ steps:
122
+ - type: tap
123
+ target: { testId: login-btn }
124
+ errorIf: not-found # Fail immediately if element not found
125
+
126
+ - type: assert
127
+ target: { testId: welcome-msg }
128
+ errorIf: not-visible # Fail if element exists but not visible
129
+
130
+ - type: input
131
+ target: { testId: email }
132
+ value: test@example.com
133
+ errorIf: disabled # Fail if input is disabled
134
+ ```
135
+
136
+ **Available conditions:**
137
+
138
+ | Condition | Description |
139
+ |-----------|-------------|
140
+ | `not-found` | Element doesn't exist in DOM |
141
+ | `not-visible` | Element exists but not visible |
142
+ | `disabled` | Element is disabled |
143
+ | `empty` | Element has no text content |
144
+
145
+ > **Note:** `testId` now matches `data-testid`, `id`, and `class` attributes in that order.
146
+
87
147
  ## Resource Cleanup
88
148
 
89
149
  IntelliTester automatically tracks resources created during tests and cleans them up after execution. This ensures tests don't leave behind database rows, files, users, or other artifacts.
@@ -215,3 +275,126 @@ intellitester run --session-id my-session --track-dir .intellitester/track
215
275
  ```
216
276
 
217
277
  If `INTELLITESTER_TRACK_URL` is set, `track()` will send HTTP requests and also append to the track file (when available). If only the file is set, `track()` writes locally.
278
+
279
+ ## Pipelines & Workflows
280
+
281
+ IntelliTester supports three levels of test organization:
282
+
283
+ ### Test Files (`*.test.yaml`)
284
+
285
+ A single test with a sequence of steps. The most basic building block.
286
+
287
+ ```yaml
288
+ name: Login Test
289
+ platform: web
290
+ variables:
291
+ EMAIL: test@example.com
292
+ steps:
293
+ - type: navigate
294
+ value: /login
295
+ - type: input
296
+ target: { testId: email }
297
+ value: ${EMAIL}
298
+ - type: input
299
+ target: { testId: password }
300
+ value: secret123
301
+ - type: tap
302
+ target: { text: Sign In }
303
+ - type: assert
304
+ target: { text: Welcome }
305
+ ```
306
+
307
+ ### Workflows (`*.workflow.yaml`)
308
+
309
+ Multiple tests run in sequence with a shared browser session. Tests share cookies, local storage, and authentication state.
310
+
311
+ ```yaml
312
+ name: User Onboarding
313
+ platform: web
314
+ config:
315
+ web:
316
+ baseUrl: http://localhost:3000
317
+ continueOnFailure: false
318
+ tests:
319
+ - file: ./signup.test.yaml
320
+ id: signup
321
+ - file: ./verify-email.test.yaml
322
+ id: verify
323
+ variables:
324
+ EMAIL: ${signup.EMAIL}
325
+ - file: ./complete-profile.test.yaml
326
+ ```
327
+
328
+ ### Pipelines (`*.pipeline.yaml`)
329
+
330
+ Multiple workflows with shared browser session, dependencies, and variables. Pipelines orchestrate complex test suites with control over execution order and failure handling.
331
+
332
+ ```yaml
333
+ name: Full E2E Suite
334
+ platform: web
335
+ on_failure: skip # skip | fail | ignore
336
+ cleanup_on_failure: true
337
+ config:
338
+ web:
339
+ baseUrl: http://localhost:3000
340
+ webServer:
341
+ command: npm run dev
342
+ url: http://localhost:3000
343
+ reuseExistingServer: true
344
+ timeout: 30000
345
+ workflows:
346
+ - file: ./auth.workflow.yaml
347
+ id: auth
348
+ on_failure: fail # Stop pipeline if auth fails
349
+
350
+ - file: ./dashboard.workflow.yaml
351
+ id: dashboard
352
+ depends_on: [auth]
353
+ variables:
354
+ USER_TOKEN: ${auth.TOKEN}
355
+
356
+ - file: ./settings.workflow.yaml
357
+ depends_on: [auth]
358
+ on_failure: ignore # Continue even if settings tests fail
359
+
360
+ - file: ./cleanup.workflow.yaml
361
+ depends_on: [dashboard, settings]
362
+ ```
363
+
364
+ ### Pipeline Features
365
+
366
+ **Workflow Properties:**
367
+
368
+ | Property | Description |
369
+ |----------|-------------|
370
+ | `file` | Path to the workflow file (required) |
371
+ | `id` | Identifier for referencing in `depends_on` and variable passing |
372
+ | `depends_on` | Array of workflow IDs that must complete first |
373
+ | `variables` | Variables to inject, can reference outputs from dependencies |
374
+ | `on_failure` | How to handle failure: `skip`, `fail`, or `ignore` |
375
+
376
+ **Failure Handling (`on_failure`):**
377
+
378
+ - `skip` - Skip dependent workflows, continue independent ones (default)
379
+ - `fail` - Stop the entire pipeline immediately
380
+ - `ignore` - Continue as if the workflow succeeded
381
+
382
+ **Web Server (`config.webServer`):**
383
+
384
+ Start a dev server automatically before running tests:
385
+
386
+ ```yaml
387
+ config:
388
+ webServer:
389
+ command: npm run dev # Command to start server
390
+ url: http://localhost:3000 # Wait for this URL
391
+ reuseExistingServer: true # Use existing if running
392
+ timeout: 30000 # Startup timeout (ms)
393
+ ```
394
+
395
+ **Shared Browser Session:**
396
+
397
+ All workflows in a pipeline share the same browser context, preserving:
398
+ - Cookies and session storage
399
+ - Authentication state
400
+ - Local storage data
@@ -1,4 +1,4 @@
1
- import { loadTestDefinition } from './chunk-64V2U2Y4.js';
1
+ import { loadTestDefinition } from './chunk-ML5CBUF7.js';
2
2
  import { track } from './chunk-2BFRS6ZZ.js';
3
3
  import { loadCleanupHandlers, executeCleanup, saveFailedCleanup } from './chunk-O4H5QO5P.js';
4
4
  import { NumberDictionary, uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator';
@@ -1036,7 +1036,11 @@ var resolveUrl = (value, baseUrl) => {
1036
1036
  }
1037
1037
  };
1038
1038
  var resolveLocator = (page, locator) => {
1039
- if (locator.testId) return page.getByTestId(locator.testId);
1039
+ if (locator.testId) {
1040
+ return page.locator(
1041
+ `[data-testid="${locator.testId}"], #${CSS.escape(locator.testId)}, .${CSS.escape(locator.testId)}`
1042
+ ).first();
1043
+ }
1040
1044
  if (locator.text) return page.getByText(locator.text);
1041
1045
  if (locator.css) return page.locator(locator.css);
1042
1046
  if (locator.xpath) return page.locator(`xpath=${locator.xpath}`);
@@ -1048,6 +1052,49 @@ var resolveLocator = (page, locator) => {
1048
1052
  if (locator.description) return page.getByText(locator.description);
1049
1053
  throw new Error("No usable selector found for locator");
1050
1054
  };
1055
+ var checkErrorIf = async (page, locator, errorIf) => {
1056
+ if (!errorIf) return;
1057
+ const handle = resolveLocator(page, locator);
1058
+ switch (errorIf) {
1059
+ case "not-found": {
1060
+ const count = await handle.count();
1061
+ if (count === 0) {
1062
+ throw new Error(`errorIf: Element not found in DOM (testId/selector: ${JSON.stringify(locator)})`);
1063
+ }
1064
+ break;
1065
+ }
1066
+ case "not-visible": {
1067
+ const count = await handle.count();
1068
+ if (count > 0) {
1069
+ const isVisible = await handle.isVisible();
1070
+ if (!isVisible) {
1071
+ throw new Error(`errorIf: Element exists but is not visible (testId/selector: ${JSON.stringify(locator)})`);
1072
+ }
1073
+ }
1074
+ break;
1075
+ }
1076
+ case "disabled": {
1077
+ const count = await handle.count();
1078
+ if (count > 0) {
1079
+ const isDisabled = await handle.isDisabled();
1080
+ if (isDisabled) {
1081
+ throw new Error(`errorIf: Element is disabled (testId/selector: ${JSON.stringify(locator)})`);
1082
+ }
1083
+ }
1084
+ break;
1085
+ }
1086
+ case "empty": {
1087
+ const count = await handle.count();
1088
+ if (count > 0) {
1089
+ const text = await handle.textContent();
1090
+ if (!text || text.trim() === "") {
1091
+ throw new Error(`errorIf: Element has no text content (testId/selector: ${JSON.stringify(locator)})`);
1092
+ }
1093
+ }
1094
+ break;
1095
+ }
1096
+ }
1097
+ };
1051
1098
  async function ensureScreenshotDir(dir) {
1052
1099
  await fs.mkdir(dir, { recursive: true });
1053
1100
  }
@@ -1379,6 +1426,7 @@ async function executeActionWithRetry(page, action, index, options) {
1379
1426
  if (debugMode) {
1380
1427
  console.log(`[DEBUG] Tapping element:`, action.target);
1381
1428
  }
1429
+ await checkErrorIf(page, action.target, action.errorIf);
1382
1430
  await runTap(page, action.target);
1383
1431
  break;
1384
1432
  }
@@ -1388,17 +1436,20 @@ async function executeActionWithRetry(page, action, index, options) {
1388
1436
  console.log(`[DEBUG] Inputting value into element:`, action.target);
1389
1437
  console.log(`[DEBUG] Value: ${interpolated}`);
1390
1438
  }
1439
+ await checkErrorIf(page, action.target, action.errorIf);
1391
1440
  await runInput(page, action.target, action.value, context);
1392
1441
  break;
1393
1442
  }
1394
1443
  case "clear": {
1395
1444
  if (debugMode) console.log(`[DEBUG] Clearing element:`, action.target);
1445
+ await checkErrorIf(page, action.target, action.errorIf);
1396
1446
  const handle = resolveLocator(page, action.target);
1397
1447
  await handle.clear();
1398
1448
  break;
1399
1449
  }
1400
1450
  case "hover": {
1401
1451
  if (debugMode) console.log(`[DEBUG] Hovering element:`, action.target);
1452
+ await checkErrorIf(page, action.target, action.errorIf);
1402
1453
  const handle = resolveLocator(page, action.target);
1403
1454
  await handle.hover();
1404
1455
  break;
@@ -1406,18 +1457,21 @@ async function executeActionWithRetry(page, action, index, options) {
1406
1457
  case "select": {
1407
1458
  const interpolated = interpolateVariables(action.value, context.variables);
1408
1459
  if (debugMode) console.log(`[DEBUG] Selecting: ${interpolated}`);
1460
+ await checkErrorIf(page, action.target, action.errorIf);
1409
1461
  const handle = resolveLocator(page, action.target);
1410
1462
  await handle.selectOption(interpolated);
1411
1463
  break;
1412
1464
  }
1413
1465
  case "check": {
1414
1466
  if (debugMode) console.log(`[DEBUG] Checking:`, action.target);
1467
+ await checkErrorIf(page, action.target, action.errorIf);
1415
1468
  const handle = resolveLocator(page, action.target);
1416
1469
  await handle.check();
1417
1470
  break;
1418
1471
  }
1419
1472
  case "uncheck": {
1420
1473
  if (debugMode) console.log(`[DEBUG] Unchecking:`, action.target);
1474
+ await checkErrorIf(page, action.target, action.errorIf);
1421
1475
  const handle = resolveLocator(page, action.target);
1422
1476
  await handle.uncheck();
1423
1477
  break;
@@ -1425,6 +1479,7 @@ async function executeActionWithRetry(page, action, index, options) {
1425
1479
  case "press": {
1426
1480
  if (debugMode) console.log(`[DEBUG] Pressing key: ${action.key}`);
1427
1481
  if (action.target) {
1482
+ await checkErrorIf(page, action.target, action.errorIf);
1428
1483
  const handle = resolveLocator(page, action.target);
1429
1484
  await handle.press(action.key);
1430
1485
  } else {
@@ -1434,6 +1489,7 @@ async function executeActionWithRetry(page, action, index, options) {
1434
1489
  }
1435
1490
  case "focus": {
1436
1491
  if (debugMode) console.log(`[DEBUG] Focusing:`, action.target);
1492
+ await checkErrorIf(page, action.target, action.errorIf);
1437
1493
  const handle = resolveLocator(page, action.target);
1438
1494
  await handle.focus();
1439
1495
  break;
@@ -1446,13 +1502,20 @@ async function executeActionWithRetry(page, action, index, options) {
1446
1502
  console.log(`[DEBUG] Expected text contains: ${interpolated}`);
1447
1503
  }
1448
1504
  }
1505
+ await checkErrorIf(page, action.target, action.errorIf);
1449
1506
  await runAssert(page, action.target, action.value, context);
1450
1507
  break;
1451
1508
  }
1452
1509
  case "wait":
1510
+ if (action.target && action.errorIf) {
1511
+ await checkErrorIf(page, action.target, action.errorIf);
1512
+ }
1453
1513
  await runWait(page, action);
1454
1514
  break;
1455
1515
  case "scroll":
1516
+ if (action.target && action.errorIf) {
1517
+ await checkErrorIf(page, action.target, action.errorIf);
1518
+ }
1456
1519
  await runScroll(page, action);
1457
1520
  break;
1458
1521
  case "screenshot":
@@ -1547,6 +1610,9 @@ async function executeActionWithRetry(page, action, index, options) {
1547
1610
  }
1548
1611
  case "waitForSelector": {
1549
1612
  const wsAction = action;
1613
+ if (wsAction.errorIf) {
1614
+ await checkErrorIf(page, wsAction.target, wsAction.errorIf);
1615
+ }
1550
1616
  const handle = resolveLocator(page, wsAction.target);
1551
1617
  const timeout = wsAction.timeout ?? 3e4;
1552
1618
  if (debugMode) {
@@ -1690,7 +1756,7 @@ async function executeActionWithRetry(page, action, index, options) {
1690
1756
  });
1691
1757
  }
1692
1758
  } else {
1693
- const { loadWorkflowDefinition, loadTestDefinition: loadTestDefinition2 } = await import('./loader-JSTO36JJ.js');
1759
+ const { loadWorkflowDefinition, loadTestDefinition: loadTestDefinition2 } = await import('./loader-B46MGIWG.js');
1694
1760
  const workflowPath = path2.resolve(process.cwd(), branchToExecute.workflow);
1695
1761
  const workflowDir = path2.dirname(workflowPath);
1696
1762
  if (debugMode) {
@@ -2646,7 +2712,7 @@ async function runTestInWorkflow(test, page, context, options, _workflowDir, wor
2646
2712
  if (debugMode) {
2647
2713
  console.log(` [DEBUG] waitForBranch: loading workflow from ${workflowPath}`);
2648
2714
  }
2649
- const { loadWorkflowDefinition } = await import('./loader-JSTO36JJ.js');
2715
+ const { loadWorkflowDefinition } = await import('./loader-B46MGIWG.js');
2650
2716
  const nestedWorkflow = await loadWorkflowDefinition(workflowPath);
2651
2717
  if (branch.variables) {
2652
2718
  for (const [key, value] of Object.entries(branch.variables)) {
@@ -3275,5 +3341,5 @@ Collected ${serverResources.length} server-tracked resources`);
3275
3341
  }
3276
3342
 
3277
3343
  export { createAIProvider, createTestContext, generateFillerText, generateRandomEmail, generateRandomPhone, generateRandomPhoto, generateRandomUsername, getBrowserLaunchOptions, initFileTracking, interpolateVariables, killServer, mergeFileTrackedResources, runWebTest, runWorkflow, runWorkflowWithContext, setupAppwriteTracking, startTrackingServer, startWebServer };
3278
- //# sourceMappingURL=chunk-EPN5OT5I.js.map
3279
- //# sourceMappingURL=chunk-EPN5OT5I.js.map
3344
+ //# sourceMappingURL=chunk-2KTVH32L.js.map
3345
+ //# sourceMappingURL=chunk-2KTVH32L.js.map