@step-forge/step-forge 0.0.21 → 0.0.23

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 (45) hide show
  1. package/RUNTIME.md +73 -12
  2. package/dist/analyzer-DIZPMxzN.cjs +441 -0
  3. package/dist/analyzer-DIZPMxzN.cjs.map +1 -0
  4. package/dist/analyzer-DnfK54Dw.js +417 -0
  5. package/dist/analyzer-DnfK54Dw.js.map +1 -0
  6. package/dist/analyzer-cli.js +1 -1
  7. package/dist/{analyzer-DYfdoSIS.js → analyzer-gYyAKIpM.js} +61 -25
  8. package/dist/analyzer-gYyAKIpM.js.map +1 -0
  9. package/dist/analyzer.d.ts +2 -0
  10. package/dist/analyzer.js +1 -1
  11. package/dist/cli.cjs +753 -84
  12. package/dist/cli.cjs.map +1 -1
  13. package/dist/cli.js +751 -84
  14. package/dist/cli.js.map +1 -1
  15. package/dist/{hooks-Dar49TtT.d.ts → config-C7PCYgYy.d.cts} +65 -16
  16. package/dist/{hooks-Dar49TtT.d.cts → config-C7PCYgYy.d.ts} +65 -16
  17. package/dist/{engine-DPRxs6eC.js → engine-DPVLEHBi.js} +8 -26
  18. package/dist/engine-DPVLEHBi.js.map +1 -0
  19. package/dist/{engine-DWAIlwWp.cjs → engine-vqA-eL_T.cjs} +8 -26
  20. package/dist/engine-vqA-eL_T.cjs.map +1 -0
  21. package/dist/{gherkinParser-CHpkEwii.cjs → gherkinParser-_ZgvELdy.cjs} +142 -3
  22. package/dist/gherkinParser-_ZgvELdy.cjs.map +1 -0
  23. package/dist/{gherkinParser-CKARHgt4.js → gherkinParser-i-Q7M6mi.js} +98 -4
  24. package/dist/gherkinParser-i-Q7M6mi.js.map +1 -0
  25. package/dist/hooks-BDCMKeNq.js +71 -0
  26. package/dist/{hooks-CywugMQQ.js.map → hooks-BDCMKeNq.js.map} +1 -1
  27. package/dist/{hooks-CGYzwDOv.cjs → hooks-Be0cjULN.cjs} +20 -31
  28. package/dist/{hooks-CGYzwDOv.cjs.map → hooks-Be0cjULN.cjs.map} +1 -1
  29. package/dist/runtime.cjs +3 -3
  30. package/dist/runtime.d.cts +11 -48
  31. package/dist/runtime.d.ts +11 -48
  32. package/dist/runtime.js +3 -3
  33. package/dist/step-forge.cjs +22 -377
  34. package/dist/step-forge.cjs.map +1 -1
  35. package/dist/step-forge.d.cts +13 -4
  36. package/dist/step-forge.d.ts +13 -4
  37. package/dist/step-forge.js +17 -371
  38. package/dist/step-forge.js.map +1 -1
  39. package/package.json +2 -2
  40. package/dist/analyzer-DYfdoSIS.js.map +0 -1
  41. package/dist/engine-DPRxs6eC.js.map +0 -1
  42. package/dist/engine-DWAIlwWp.cjs.map +0 -1
  43. package/dist/gherkinParser-CHpkEwii.cjs.map +0 -1
  44. package/dist/gherkinParser-CKARHgt4.js.map +0 -1
  45. package/dist/hooks-CywugMQQ.js +0 -82
@@ -1,4 +1,4 @@
1
- import { C as numberParser, S as intParser, _ as StepDefinitionMeta, b as Parser, d as AnalysisRule, f as AnalyzerConfig, g as ParsedStep, h as ParsedScenario, i as PlainHookFn, m as MatchedStep, o as ScenarioHookFn, p as Diagnostic, s as ScenarioInfo, v as BasicWorld, w as stringParser, x as booleanParser, y as MergeableWorld } from "./hooks-Dar49TtT.cjs";
1
+ import { C as booleanParser, E as stringParser, S as Parser, T as numberParser, _ as ParsedScenario, b as BasicWorld, c as ScenarioHookFn, g as MatchedStep, h as Diagnostic, l as ScenarioInfo, m as AnalyzerConfig, n as RunnerOptions, o as PlainHookFn, p as AnalysisRule, v as ParsedStep, w as intParser, x as MergeableWorld, y as StepDefinitionMeta } from "./config-C7PCYgYy.cjs";
2
2
 
3
3
  //#region src/builderTypeUtils.d.ts
4
4
  type StepType = "given" | "when" | "then";
@@ -470,9 +470,18 @@ declare function afterScenario<World extends MergeableWorld<any, any, any> = Mer
470
470
  declare function beforeFeature(fn: PlainHookFn): void;
471
471
  /** Run once at the end of each feature file (reverse registration order). */
472
472
  declare function afterFeature(fn: PlainHookFn): void;
473
- /** Run once before the entire test run, across all feature files. */
473
+ /**
474
+ * Run once before the entire run, ahead of any scenario (and before
475
+ * concurrency starts). Multiple `beforeAll` hooks run **in parallel** with no
476
+ * ordering between them — if a step of setup must precede another, sequence both
477
+ * inside a single hook.
478
+ */
474
479
  declare function beforeAll(fn: PlainHookFn): void;
475
- /** Run once after the entire test run (reverse registration order). */
480
+ /**
481
+ * Run once after the entire run, once every scenario is done. Multiple
482
+ * `afterAll` hooks run **in parallel** with no ordering between them — sequence
483
+ * dependent teardown inside a single hook.
484
+ */
476
485
  declare function afterAll(fn: PlainHookFn): void;
477
486
  //#endregion
478
487
  //#region src/typeHelpers.d.ts
@@ -501,5 +510,5 @@ declare const analyzer: {
501
510
  defaultRules: AnalysisRule[];
502
511
  };
503
512
  //#endregion
504
- export { type AnalysisRule, type AnalyzeOptions, type AnalyzerConfig, BasicWorld, type Diagnostic, type MatchedStep, type ParsedScenario, type ParsedStep, type Parser, type PlainHookFn, type ScenarioHookFn, type ScenarioInfo, type StateFromDependencies, type StepDefinitionMeta, afterAll, afterFeature, afterScenario, analyzer, beforeAll, beforeFeature, beforeScenario, booleanParser, createBuilders, givenBuilder, intParser, numberParser, stringParser, thenBuilder, whenBuilder };
513
+ export { type AnalysisRule, type AnalyzeOptions, type AnalyzerConfig, BasicWorld, type Diagnostic, type MatchedStep, type ParsedScenario, type ParsedStep, type Parser, type PlainHookFn, type RunnerOptions, type ScenarioHookFn, type ScenarioInfo, type StateFromDependencies, type StepDefinitionMeta, afterAll, afterFeature, afterScenario, analyzer, beforeAll, beforeFeature, beforeScenario, booleanParser, createBuilders, givenBuilder, intParser, numberParser, stringParser, thenBuilder, whenBuilder };
505
514
  //# sourceMappingURL=step-forge.d.cts.map
@@ -1,4 +1,4 @@
1
- import { C as numberParser, S as intParser, _ as StepDefinitionMeta, b as Parser, d as AnalysisRule, f as AnalyzerConfig, g as ParsedStep, h as ParsedScenario, i as PlainHookFn, m as MatchedStep, o as ScenarioHookFn, p as Diagnostic, s as ScenarioInfo, v as BasicWorld, w as stringParser, x as booleanParser, y as MergeableWorld } from "./hooks-Dar49TtT.js";
1
+ import { C as booleanParser, E as stringParser, S as Parser, T as numberParser, _ as ParsedScenario, b as BasicWorld, c as ScenarioHookFn, g as MatchedStep, h as Diagnostic, l as ScenarioInfo, m as AnalyzerConfig, n as RunnerOptions, o as PlainHookFn, p as AnalysisRule, v as ParsedStep, w as intParser, x as MergeableWorld, y as StepDefinitionMeta } from "./config-C7PCYgYy.js";
2
2
 
3
3
  //#region src/builderTypeUtils.d.ts
4
4
  type StepType = "given" | "when" | "then";
@@ -470,9 +470,18 @@ declare function afterScenario<World extends MergeableWorld<any, any, any> = Mer
470
470
  declare function beforeFeature(fn: PlainHookFn): void;
471
471
  /** Run once at the end of each feature file (reverse registration order). */
472
472
  declare function afterFeature(fn: PlainHookFn): void;
473
- /** Run once before the entire test run, across all feature files. */
473
+ /**
474
+ * Run once before the entire run, ahead of any scenario (and before
475
+ * concurrency starts). Multiple `beforeAll` hooks run **in parallel** with no
476
+ * ordering between them — if a step of setup must precede another, sequence both
477
+ * inside a single hook.
478
+ */
474
479
  declare function beforeAll(fn: PlainHookFn): void;
475
- /** Run once after the entire test run (reverse registration order). */
480
+ /**
481
+ * Run once after the entire run, once every scenario is done. Multiple
482
+ * `afterAll` hooks run **in parallel** with no ordering between them — sequence
483
+ * dependent teardown inside a single hook.
484
+ */
476
485
  declare function afterAll(fn: PlainHookFn): void;
477
486
  //#endregion
478
487
  //#region src/typeHelpers.d.ts
@@ -501,5 +510,5 @@ declare const analyzer: {
501
510
  defaultRules: AnalysisRule[];
502
511
  };
503
512
  //#endregion
504
- export { type AnalysisRule, type AnalyzeOptions, type AnalyzerConfig, BasicWorld, type Diagnostic, type MatchedStep, type ParsedScenario, type ParsedStep, type Parser, type PlainHookFn, type ScenarioHookFn, type ScenarioInfo, type StateFromDependencies, type StepDefinitionMeta, afterAll, afterFeature, afterScenario, analyzer, beforeAll, beforeFeature, beforeScenario, booleanParser, createBuilders, givenBuilder, intParser, numberParser, stringParser, thenBuilder, whenBuilder };
513
+ export { type AnalysisRule, type AnalyzeOptions, type AnalyzerConfig, BasicWorld, type Diagnostic, type MatchedStep, type ParsedScenario, type ParsedStep, type Parser, type PlainHookFn, type RunnerOptions, type ScenarioHookFn, type ScenarioInfo, type StateFromDependencies, type StepDefinitionMeta, afterAll, afterFeature, afterScenario, analyzer, beforeAll, beforeFeature, beforeScenario, booleanParser, createBuilders, givenBuilder, intParser, numberParser, stringParser, thenBuilder, whenBuilder };
505
514
  //# sourceMappingURL=step-forge.d.ts.map
@@ -1,7 +1,7 @@
1
- import { o as globalRegistry, r as globalHookRegistry } from "./hooks-CywugMQQ.js";
2
- import { i as BasicWorld, n as parseFeatureFiles, r as globFiles, t as parseFeatureContent } from "./gherkinParser-CKARHgt4.js";
1
+ import { a as matchScenarioSteps, i as findMatchingDefinitions, o as extractStepDefinitions, r as defaultRules, t as analyze } from "./analyzer-DnfK54Dw.js";
2
+ import { n as globalHookRegistry, o as globalRegistry } from "./hooks-BDCMKeNq.js";
3
+ import { a as BasicWorld, n as parseFeatureContent, o as captureDefinitionSite, r as parseFeatureFiles } from "./gherkinParser-i-Q7M6mi.js";
3
4
  import _ from "lodash";
4
- import ts from "typescript";
5
5
  //#region src/builderTypeUtils.ts
6
6
  const isString = (statement) => typeof statement === "string";
7
7
  //#endregion
@@ -114,11 +114,13 @@ const addStep = (statement, stepType, dependencies = {
114
114
  });
115
115
  world[stepType].merge({ ...result });
116
116
  };
117
+ const source = captureDefinitionSite();
117
118
  globalRegistry.add({
118
119
  stepType,
119
120
  expression,
120
121
  parsers,
121
- execute
122
+ execute,
123
+ source
122
124
  });
123
125
  return {
124
126
  statement,
@@ -192,371 +194,6 @@ const thenStatement = () => (statement) => {
192
194
  };
193
195
  const thenBuilder = () => ({ statement: thenStatement() });
194
196
  //#endregion
195
- //#region src/analyzer/stepExtractor.ts
196
- const BUILDER_NAMES = {
197
- givenBuilder: "given",
198
- whenBuilder: "when",
199
- thenBuilder: "then"
200
- };
201
- function extractStepDefinitions(filePaths, tsConfigPath) {
202
- const configPath = tsConfigPath ?? ts.findConfigFile(process.cwd(), ts.sys.fileExists);
203
- let compilerOptions = {
204
- target: ts.ScriptTarget.ESNext,
205
- module: ts.ModuleKind.ESNext,
206
- moduleResolution: ts.ModuleResolutionKind.Node10,
207
- strict: true,
208
- esModuleInterop: true
209
- };
210
- if (configPath) {
211
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
212
- if (!configFile.error) compilerOptions = ts.parseJsonConfigFileContent(configFile.config, ts.sys, configPath.replace(/[/\\][^/\\]+$/, "")).options;
213
- }
214
- compilerOptions.noEmit = true;
215
- const program = ts.createProgram(filePaths, compilerOptions);
216
- const checker = program.getTypeChecker();
217
- const results = [];
218
- for (const filePath of filePaths) {
219
- const sourceFile = program.getSourceFile(filePath);
220
- if (!sourceFile) continue;
221
- const fileResults = extractFromSourceFile(sourceFile, checker);
222
- results.push(...fileResults);
223
- }
224
- return results;
225
- }
226
- function extractFromSourceFile(sourceFile, checker) {
227
- const results = [];
228
- function visit(node) {
229
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "step") {
230
- const meta = extractFromRegisterCall(node, sourceFile, checker);
231
- if (meta) results.push(meta);
232
- }
233
- ts.forEachChild(node, visit);
234
- }
235
- visit(sourceFile);
236
- return results;
237
- }
238
- function extractFromRegisterCall(registerCall, sourceFile, checker) {
239
- const chain = collectCallChain(registerCall);
240
- let stepType = null;
241
- let expression = null;
242
- let dependencies = {
243
- given: {},
244
- when: {},
245
- then: {}
246
- };
247
- let produces = [];
248
- for (const link of chain) {
249
- const name = getCallName(link);
250
- if (!name) continue;
251
- if (name === "register") continue;
252
- if (name === "step") {
253
- produces = extractProducedKeys(link, checker);
254
- continue;
255
- }
256
- if (name === "dependencies") {
257
- dependencies = extractDependencies(link);
258
- continue;
259
- }
260
- if (name === "statement") {
261
- expression = extractExpression(link);
262
- continue;
263
- }
264
- if (BUILDER_NAMES[name]) {
265
- stepType = BUILDER_NAMES[name];
266
- continue;
267
- }
268
- }
269
- if (!stepType || !expression) {
270
- const reExport = resolveReExportedCall(chain, checker);
271
- if (reExport) {
272
- if (!stepType) stepType = reExport.stepType;
273
- if (!expression) expression = reExport.expression;
274
- }
275
- }
276
- if (!stepType || !expression) return null;
277
- const line = sourceFile.getLineAndCharacterOfPosition(registerCall.getStart()).line + 1;
278
- return {
279
- stepType,
280
- expression,
281
- dependencies,
282
- produces,
283
- sourceFile: sourceFile.fileName,
284
- line
285
- };
286
- }
287
- /**
288
- * Collect all call expressions in the method chain, from register() back to the origin.
289
- */
290
- function collectCallChain(call) {
291
- const chain = [call];
292
- let current = call.expression;
293
- while (true) {
294
- if (ts.isPropertyAccessExpression(current)) current = current.expression;
295
- if (ts.isCallExpression(current)) {
296
- chain.push(current);
297
- current = current.expression;
298
- } else break;
299
- }
300
- return chain;
301
- }
302
- function getCallName(call) {
303
- const expr = call.expression;
304
- if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
305
- if (ts.isIdentifier(expr)) return expr.text;
306
- return null;
307
- }
308
- function extractExpression(statementCall) {
309
- const arg = statementCall.arguments[0];
310
- if (!arg) return null;
311
- if (ts.isStringLiteral(arg)) return arg.text;
312
- if (ts.isArrowFunction(arg)) return extractExpressionFromArrowFunction(arg);
313
- return null;
314
- }
315
- function extractExpressionFromArrowFunction(fn) {
316
- const params = fn.parameters.map((p) => p.name.getText());
317
- let body = fn.body;
318
- if (ts.isBlock(body)) {
319
- const returnStmt = body.statements.find(ts.isReturnStatement);
320
- if (returnStmt?.expression) body = returnStmt.expression;
321
- else return null;
322
- }
323
- if (ts.isTemplateExpression(body)) return reconstructExpressionFromTemplate(body, params);
324
- if (ts.isNoSubstitutionTemplateLiteral(body)) return body.text;
325
- return null;
326
- }
327
- function reconstructExpressionFromTemplate(template, paramNames) {
328
- let result = template.head.text;
329
- for (const span of template.templateSpans) {
330
- if (ts.isIdentifier(span.expression) && paramNames.includes(span.expression.text)) result += "{string}";
331
- else result += "{string}";
332
- result += span.literal.text;
333
- }
334
- return result;
335
- }
336
- function extractDependencies(depsCall) {
337
- const deps = {
338
- given: {},
339
- when: {},
340
- then: {}
341
- };
342
- const arg = depsCall.arguments[0];
343
- if (!arg || !ts.isObjectLiteralExpression(arg)) return deps;
344
- for (const prop of arg.properties) {
345
- if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
346
- const phase = prop.name.text;
347
- if (!deps[phase]) continue;
348
- if (ts.isObjectLiteralExpression(prop.initializer)) {
349
- for (const innerProp of prop.initializer.properties) if (ts.isPropertyAssignment(innerProp) && ts.isIdentifier(innerProp.name) && ts.isStringLiteral(innerProp.initializer)) {
350
- const val = innerProp.initializer.text;
351
- if (val === "required" || val === "optional") deps[phase][innerProp.name.text] = val;
352
- }
353
- }
354
- }
355
- return deps;
356
- }
357
- function extractProducedKeys(stepCall, checker) {
358
- const callback = stepCall.arguments[0];
359
- if (!callback) return [];
360
- if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) return extractProducedKeysFromCallback(callback, checker);
361
- return [];
362
- }
363
- function extractProducedKeysFromCallback(callback, checker) {
364
- const body = callback.body;
365
- if (!ts.isBlock(body)) return extractKeysFromExpression(body, checker);
366
- const keys = /* @__PURE__ */ new Set();
367
- function visitReturn(node) {
368
- if (ts.isReturnStatement(node) && node.expression) for (const key of extractKeysFromExpression(node.expression, checker)) keys.add(key);
369
- ts.forEachChild(node, visitReturn);
370
- }
371
- visitReturn(body);
372
- return [...keys];
373
- }
374
- function extractKeysFromExpression(expr, checker) {
375
- while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
376
- if (ts.isObjectLiteralExpression(expr)) return expr.properties.filter((p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)).map((p) => p.name.getText()).filter(Boolean);
377
- try {
378
- return checker.getTypeAtLocation(expr).getProperties().map((p) => p.name).filter((n) => n !== "merge");
379
- } catch {
380
- return [];
381
- }
382
- }
383
- function resolveReExportedCall(chain, checker) {
384
- const lastCall = chain[chain.length - 1];
385
- if (!lastCall) return null;
386
- const expr = lastCall.expression;
387
- let identifier = null;
388
- if (ts.isIdentifier(expr)) identifier = expr;
389
- else if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression)) identifier = expr.expression;
390
- if (!identifier) return null;
391
- const symbol = checker.getSymbolAtLocation(identifier);
392
- if (!symbol) return null;
393
- const decl = symbol.valueDeclaration;
394
- if (!decl || !ts.isVariableDeclaration(decl) || !decl.initializer) return null;
395
- const init = decl.initializer;
396
- if (ts.isPropertyAccessExpression(init) && init.name.text === "statement") {
397
- const callExpr = init.expression;
398
- if (ts.isCallExpression(callExpr)) {
399
- const callee = callExpr.expression;
400
- if (ts.isIdentifier(callee) && BUILDER_NAMES[callee.text]) {
401
- const expression = extractExpression(lastCall);
402
- return {
403
- stepType: BUILDER_NAMES[callee.text],
404
- expression
405
- };
406
- }
407
- }
408
- }
409
- return null;
410
- }
411
- //#endregion
412
- //#region src/analyzer/stepMatcher.ts
413
- function compileDefinitions(definitions) {
414
- const compiled = [];
415
- for (const def of definitions) try {
416
- const placeholder = "###PLACEHOLDER###";
417
- const regexStr = def.expression.replace(/\{[^}]+\}/g, placeholder).replace(/[.*+?^$()|[\]\\]/g, "\\$&").replace(new RegExp(placeholder, "g"), "(.+)");
418
- compiled.push({
419
- regex: new RegExp(`^${regexStr}$`, "i"),
420
- definition: def
421
- });
422
- } catch {}
423
- return compiled;
424
- }
425
- function matchScenarioSteps(scenario, definitions) {
426
- const compiled = compileDefinitions(definitions);
427
- return scenario.steps.map((step) => {
428
- const matches = findMatches(step.text, step.effectiveKeyword, compiled);
429
- return {
430
- ...step,
431
- definitions: matches
432
- };
433
- });
434
- }
435
- function findMatchingDefinitions(text, effectiveKeyword, definitions) {
436
- return findMatches(text, effectiveKeyword, compileDefinitions(definitions));
437
- }
438
- function findMatches(text, effectiveKeyword, compiled) {
439
- const expectedStepType = {
440
- Given: "given",
441
- When: "when",
442
- Then: "then"
443
- }[effectiveKeyword];
444
- const typedMatches = [];
445
- for (const { regex, definition } of compiled) {
446
- if (definition.stepType !== expectedStepType) continue;
447
- if (regex.test(text)) typedMatches.push(definition);
448
- }
449
- if (typedMatches.length > 0) return typedMatches;
450
- const fallbackMatches = [];
451
- for (const { regex, definition } of compiled) if (regex.test(text)) fallbackMatches.push(definition);
452
- return fallbackMatches;
453
- }
454
- //#endregion
455
- //#region src/analyzer/rules/index.ts
456
- const defaultRules = [
457
- {
458
- name: "undefined-step",
459
- check(scenario, matchedSteps) {
460
- return matchedSteps.filter((step) => step.definitions.length === 0).map((step) => ({
461
- file: scenario.file,
462
- range: {
463
- startLine: step.line,
464
- startColumn: step.column,
465
- endLine: step.line,
466
- endColumn: step.column + step.text.length
467
- },
468
- severity: "error",
469
- message: `Step "${step.text}" does not match any step definition`,
470
- rule: "undefined-step",
471
- source: "step-forge"
472
- }));
473
- }
474
- },
475
- {
476
- name: "ambiguous-step",
477
- check(scenario, matchedSteps) {
478
- return matchedSteps.filter((step) => step.definitions.length > 1).map((step) => {
479
- const locations = step.definitions.map((d) => `${d.sourceFile}:${d.line}`).join(", ");
480
- return {
481
- file: scenario.file,
482
- range: {
483
- startLine: step.line,
484
- startColumn: step.column,
485
- endLine: step.line,
486
- endColumn: step.column + step.text.length
487
- },
488
- severity: "error",
489
- message: `Step "${step.text}" matches multiple step definitions: ${locations}`,
490
- rule: "ambiguous-step",
491
- source: "step-forge"
492
- };
493
- });
494
- }
495
- },
496
- {
497
- name: "dependency-check",
498
- check(scenario, matchedSteps) {
499
- const diagnostics = [];
500
- const produced = {
501
- given: /* @__PURE__ */ new Set(),
502
- when: /* @__PURE__ */ new Set(),
503
- then: /* @__PURE__ */ new Set()
504
- };
505
- for (const step of matchedSteps) {
506
- if (step.definitions.length !== 1) continue;
507
- const { dependencies, produces, stepType } = step.definitions[0];
508
- const missing = {};
509
- for (const phase of [
510
- "given",
511
- "when",
512
- "then"
513
- ]) for (const [key, requirement] of Object.entries(dependencies[phase])) if (requirement === "required" && !produced[phase].has(key)) {
514
- if (!missing[phase]) missing[phase] = [];
515
- missing[phase].push(key);
516
- }
517
- if (Object.keys(missing).length > 0) {
518
- const message = "Missing required dependencies:\n" + Object.entries(missing).map(([phase, keys]) => `${phase.charAt(0).toUpperCase() + phase.slice(1)}: ${keys.map((k) => `${phase}.${k}`).join(", ")}`).join("\n");
519
- diagnostics.push({
520
- file: scenario.file,
521
- range: {
522
- startLine: step.line,
523
- startColumn: step.column,
524
- endLine: step.line,
525
- endColumn: step.column + step.text.length
526
- },
527
- severity: "error",
528
- message,
529
- rule: "dependency-check",
530
- source: "step-forge"
531
- });
532
- }
533
- for (const key of produces) produced[stepType].add(key);
534
- }
535
- return diagnostics;
536
- }
537
- }
538
- ];
539
- function runRules(rules, scenario, matchedSteps) {
540
- return rules.flatMap((rule) => rule.check(scenario, matchedSteps));
541
- }
542
- //#endregion
543
- //#region src/analyzer/index.ts
544
- async function analyze(config, options) {
545
- const rules = options?.rules ?? defaultRules;
546
- const stepFilePaths = await globFiles(config.stepFiles);
547
- const featureFilePaths = await globFiles(config.featureFiles);
548
- if (stepFilePaths.length === 0) return [];
549
- if (featureFilePaths.length === 0) return [];
550
- const stepDefinitions = extractStepDefinitions(stepFilePaths, config.tsConfigPath);
551
- const scenarios = parseFeatureFiles(featureFilePaths);
552
- const diagnostics = [];
553
- for (const scenario of scenarios) {
554
- const scenarioDiags = runRules(rules, scenario, matchScenarioSteps(scenario, stepDefinitions));
555
- diagnostics.push(...scenarioDiags);
556
- }
557
- return diagnostics;
558
- }
559
- //#endregion
560
197
  //#region src/init.ts
561
198
  const createBuilders = () => {
562
199
  return {
@@ -608,7 +245,12 @@ function afterFeature(fn) {
608
245
  fn
609
246
  });
610
247
  }
611
- /** Run once before the entire test run, across all feature files. */
248
+ /**
249
+ * Run once before the entire run, ahead of any scenario (and before
250
+ * concurrency starts). Multiple `beforeAll` hooks run **in parallel** with no
251
+ * ordering between them — if a step of setup must precede another, sequence both
252
+ * inside a single hook.
253
+ */
612
254
  function beforeAll(fn) {
613
255
  globalHookRegistry.add({
614
256
  scope: "global",
@@ -616,7 +258,11 @@ function beforeAll(fn) {
616
258
  fn
617
259
  });
618
260
  }
619
- /** Run once after the entire test run (reverse registration order). */
261
+ /**
262
+ * Run once after the entire run, once every scenario is done. Multiple
263
+ * `afterAll` hooks run **in parallel** with no ordering between them — sequence
264
+ * dependent teardown inside a single hook.
265
+ */
620
266
  function afterAll(fn) {
621
267
  globalHookRegistry.add({
622
268
  scope: "global",