@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.
- package/RUNTIME.md +73 -12
- package/dist/analyzer-DIZPMxzN.cjs +441 -0
- package/dist/analyzer-DIZPMxzN.cjs.map +1 -0
- package/dist/analyzer-DnfK54Dw.js +417 -0
- package/dist/analyzer-DnfK54Dw.js.map +1 -0
- package/dist/analyzer-cli.js +1 -1
- package/dist/{analyzer-DYfdoSIS.js → analyzer-gYyAKIpM.js} +61 -25
- package/dist/analyzer-gYyAKIpM.js.map +1 -0
- package/dist/analyzer.d.ts +2 -0
- package/dist/analyzer.js +1 -1
- package/dist/cli.cjs +753 -84
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +751 -84
- package/dist/cli.js.map +1 -1
- package/dist/{hooks-Dar49TtT.d.ts → config-C7PCYgYy.d.cts} +65 -16
- package/dist/{hooks-Dar49TtT.d.cts → config-C7PCYgYy.d.ts} +65 -16
- package/dist/{engine-DPRxs6eC.js → engine-DPVLEHBi.js} +8 -26
- package/dist/engine-DPVLEHBi.js.map +1 -0
- package/dist/{engine-DWAIlwWp.cjs → engine-vqA-eL_T.cjs} +8 -26
- package/dist/engine-vqA-eL_T.cjs.map +1 -0
- package/dist/{gherkinParser-CHpkEwii.cjs → gherkinParser-_ZgvELdy.cjs} +142 -3
- package/dist/gherkinParser-_ZgvELdy.cjs.map +1 -0
- package/dist/{gherkinParser-CKARHgt4.js → gherkinParser-i-Q7M6mi.js} +98 -4
- package/dist/gherkinParser-i-Q7M6mi.js.map +1 -0
- package/dist/hooks-BDCMKeNq.js +71 -0
- package/dist/{hooks-CywugMQQ.js.map → hooks-BDCMKeNq.js.map} +1 -1
- package/dist/{hooks-CGYzwDOv.cjs → hooks-Be0cjULN.cjs} +20 -31
- package/dist/{hooks-CGYzwDOv.cjs.map → hooks-Be0cjULN.cjs.map} +1 -1
- package/dist/runtime.cjs +3 -3
- package/dist/runtime.d.cts +11 -48
- package/dist/runtime.d.ts +11 -48
- package/dist/runtime.js +3 -3
- package/dist/step-forge.cjs +22 -377
- package/dist/step-forge.cjs.map +1 -1
- package/dist/step-forge.d.cts +13 -4
- package/dist/step-forge.d.ts +13 -4
- package/dist/step-forge.js +17 -371
- package/dist/step-forge.js.map +1 -1
- package/package.json +2 -2
- package/dist/analyzer-DYfdoSIS.js.map +0 -1
- package/dist/engine-DPRxs6eC.js.map +0 -1
- package/dist/engine-DWAIlwWp.cjs.map +0 -1
- package/dist/gherkinParser-CHpkEwii.cjs.map +0 -1
- package/dist/gherkinParser-CKARHgt4.js.map +0 -1
- package/dist/hooks-CywugMQQ.js +0 -82
package/dist/step-forge.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
package/dist/step-forge.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
package/dist/step-forge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { o as
|
|
2
|
-
import {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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",
|