@autometa/test-builder 0.4.1 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,729 +1,1127 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
- var __decorateClass = (decorators, target, key, kind) => {
20
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
21
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
22
- if (decorator = decorators[i])
23
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
24
- if (kind && result)
25
- __defProp(target, key, result);
26
- return result;
27
- };
28
- var __accessCheck = (obj, member, msg) => {
29
- if (!member.has(obj))
30
- throw TypeError("Cannot " + msg);
31
- };
32
- var __privateAdd = (obj, member, value) => {
33
- if (member.has(obj))
34
- throw TypeError("Cannot add the same private member more than once");
35
- member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
36
- };
37
- var __privateMethod = (obj, member, method) => {
38
- __accessCheck(obj, member, "access private method");
39
- return method;
40
- };
1
+ import { RegularExpression, CucumberExpression, ParameterTypeRegistry } from '@cucumber/cucumber-expressions';
2
+ import { createDefaultParameterTypes } from '@autometa/cucumber-expressions';
3
+ import path from 'node:path';
41
4
 
42
- // src/index.ts
43
- var src_exports = {};
44
- __export(src_exports, {
45
- BackgroundBridge: () => BackgroundBridge,
46
- ExampleBridge: () => ExampleBridge,
47
- ExamplesBridge: () => ExamplesBridge,
48
- FeatureBridge: () => FeatureBridge,
49
- GherkinCodeBridge: () => GherkinCodeBridge,
50
- GherkinWalker: () => GherkinWalker,
51
- GlobalBridge: () => GlobalBridge,
52
- Query: () => Query,
53
- RuleBridge: () => RuleBridge,
54
- ScenarioBridge: () => ScenarioBridge,
55
- ScenarioOutlineBridge: () => ScenarioOutlineBridge,
56
- StepBridge: () => StepBridge,
57
- TestBuilder: () => TestBuilder,
58
- find: () => find,
59
- findExamplesOrChild: () => findExamplesOrChild,
60
- findRuleOrChild: () => findRuleOrChild,
61
- findRuleTypes: () => findRuleTypes,
62
- findScenario: () => findScenario,
63
- findScenarioOutlineOrChild: () => findScenarioOutlineOrChild,
64
- findTestTypes: () => findTestTypes,
65
- scope: () => scope
66
- });
67
- module.exports = __toCommonJS(src_exports);
68
-
69
- // src/bridges/bridge.ts
70
- var import_gherkin = require("@autometa/gherkin");
71
- var GherkinCodeBridge = class {
72
- };
73
- var GlobalBridge = class extends GherkinCodeBridge {
74
- constructor(scope2) {
75
- super();
76
- const nullNode = class extends import_gherkin.GherkinNode {
77
- constructor() {
78
- super(...arguments);
79
- this.keyword = "none";
5
+ // src/internal/test-plan-builder.ts
6
+ function normalizeName(value) {
7
+ return value.trim();
8
+ }
9
+ function normalizeKeyword(keyword) {
10
+ return keyword.trim();
11
+ }
12
+ function normalizeUri(uri) {
13
+ const cleaned = uri.replace(/^file:/, "");
14
+ const normalized = path.normalize(cleaned);
15
+ return normalized.replace(/[\\/]+/g, "/");
16
+ }
17
+ function buildScopeSuffix(id) {
18
+ return ` [${id}]`;
19
+ }
20
+ function buildExampleSuffix(exampleId, index) {
21
+ return ` [${exampleId}#${index + 1}]`;
22
+ }
23
+ function buildQualifiedName(segments) {
24
+ return segments.map((segment) => {
25
+ const keyword = segment.keyword.trim();
26
+ const name = segment.name?.trim();
27
+ const suffix = segment.suffix ?? "";
28
+ if (!name || name.length === 0) {
29
+ return `${keyword}${suffix}`;
30
+ }
31
+ return `${keyword}: ${name}${suffix}`;
32
+ }).join(" > ");
33
+ }
34
+ function collectTags(...sources) {
35
+ const result = [];
36
+ const seen = /* @__PURE__ */ new Set();
37
+ for (const source of sources) {
38
+ if (!source) {
39
+ continue;
40
+ }
41
+ for (const tag of source) {
42
+ if (!seen.has(tag)) {
43
+ seen.add(tag);
44
+ result.push(tag);
80
45
  }
81
- };
82
- this.data = {
83
- scope: scope2,
84
- gherkin: new nullNode()
85
- };
86
- }
87
- };
88
- var FeatureBridge = class extends GherkinCodeBridge {
89
- constructor() {
90
- super(...arguments);
91
- this.scenarios = [];
92
- this.rules = [];
93
- this.steps = [];
94
- }
95
- accumulateTags() {
96
- const scenarioTags = this.scenarios.map((scenario) => scenario.accumulateTags()).flat();
97
- const ruleTags = this.rules.map((rule) => rule.accumulateTags()).flat();
98
- const allTags = [...this.data.gherkin.tags, ...scenarioTags, ...ruleTags];
99
- return [...new Set(allTags)];
100
- }
101
- };
102
- var BackgroundBridge = class extends GherkinCodeBridge {
103
- constructor() {
104
- super(...arguments);
105
- this.steps = [];
46
+ }
106
47
  }
107
- };
108
- var RuleBridge = class extends GherkinCodeBridge {
109
- constructor() {
110
- super(...arguments);
111
- this.scenarios = [];
112
- this.steps = [];
48
+ return result;
49
+ }
50
+ function combineSteps(featureSteps, ruleSteps, scenarioSteps) {
51
+ return [
52
+ ...featureSteps ?? [],
53
+ ...ruleSteps ?? [],
54
+ ...scenarioSteps ?? []
55
+ ];
56
+ }
57
+ function cloneData(data) {
58
+ if (!data) {
59
+ return void 0;
113
60
  }
114
- accumulateTags() {
115
- const scenarioTags = this.scenarios.map((scenario) => scenario.accumulateTags()).flat();
116
- const allTags = [...this.data.gherkin.tags, ...scenarioTags];
117
- return [...new Set(allTags)];
61
+ return { ...data };
62
+ }
63
+ function mergeData(base, extra) {
64
+ if (!base && !extra) {
65
+ return void 0;
118
66
  }
119
- };
120
- var ScenarioBridge = class extends GherkinCodeBridge {
121
- constructor() {
122
- super(...arguments);
123
- this.steps = [];
124
- this.report = {};
67
+ return {
68
+ ...base ?? {},
69
+ ...extra ?? {}
70
+ };
71
+ }
72
+ function createExampleData(group, compiled) {
73
+ const headers = group.tableHeader ?? [];
74
+ const rows = group.tableBody ?? [];
75
+ const row = rows[compiled.exampleIndex];
76
+ if (!row) {
77
+ return void 0;
78
+ }
79
+ const values = {};
80
+ headers.forEach((header, index) => {
81
+ const key = header.trim();
82
+ if (key.length === 0) {
83
+ return;
84
+ }
85
+ values[key] = row[index] ?? "";
86
+ });
87
+ return {
88
+ example: {
89
+ group: {
90
+ id: group.id,
91
+ name: group.name,
92
+ tags: [...group.tags]
93
+ },
94
+ index: compiled.exampleIndex,
95
+ values
96
+ }
97
+ };
98
+ }
99
+ function normalizeError(error) {
100
+ if (error instanceof Error) {
101
+ return error;
125
102
  }
126
- get title() {
127
- return this.data.scope.title(this.data.gherkin);
103
+ if (typeof error === "string") {
104
+ return new Error(error);
128
105
  }
129
- get tags() {
130
- return [...this.data.gherkin.tags];
106
+ return new Error(JSON.stringify(error));
107
+ }
108
+ function groupCompiledScenarios(compiled) {
109
+ const map = /* @__PURE__ */ new Map();
110
+ for (const scenario of compiled ?? []) {
111
+ const list = map.get(scenario.exampleGroupId) ?? [];
112
+ list.push(scenario);
113
+ map.set(scenario.exampleGroupId, list);
114
+ }
115
+ return map;
116
+ }
117
+ function isRule(element) {
118
+ return "elements" in element && Array.isArray(element.elements);
119
+ }
120
+ function isScenarioOutline(element) {
121
+ return "exampleGroups" in element;
122
+ }
123
+ function isScenario(element) {
124
+ return "steps" in element && !("exampleGroups" in element) && !("elements" in element);
125
+ }
126
+
127
+ // src/internal/nodes.ts
128
+ function createFeatureNode(init) {
129
+ return new FeatureNodeImpl(init);
130
+ }
131
+ function createRuleNode(init) {
132
+ return new RuleNodeImpl(init);
133
+ }
134
+ function createScenarioNode(init) {
135
+ return new ScenarioNodeImpl(init);
136
+ }
137
+ function createScenarioOutlineNode(init) {
138
+ return new ScenarioOutlineNodeImpl(init);
139
+ }
140
+ function createScenarioOutlineExample(init) {
141
+ return new ScenarioOutlineExampleImpl(init);
142
+ }
143
+ var FeatureNodeImpl = class {
144
+ constructor(init) {
145
+ this.type = "feature";
146
+ this.feature = init.feature;
147
+ this.scope = init.scope;
148
+ this.executions = init.executions;
149
+ this.scenarios = init.scenarios;
150
+ this.scenarioOutlines = init.outlines;
151
+ this.rules = init.rules;
152
+ this.name = init.feature.name;
153
+ this.keyword = normalizeKeyword(init.feature.keyword ?? "Feature");
154
+ if (init.feature.background) {
155
+ this.background = init.feature.background;
156
+ }
131
157
  }
132
- accumulateTags() {
133
- return this.tags;
158
+ listExecutables() {
159
+ return this.executions;
134
160
  }
135
161
  };
136
- var ExampleBridge = class extends GherkinCodeBridge {
137
- constructor() {
138
- super(...arguments);
139
- this.steps = [];
140
- this.report = {};
141
- }
142
- get title() {
143
- return this.data.scope.title(this.data.gherkin);
144
- }
145
- get tags() {
146
- return [...this.data.gherkin.tags];
147
- }
148
- accumulateTags() {
149
- return this.tags;
162
+ var RuleNodeImpl = class {
163
+ constructor(init) {
164
+ this.type = "rule";
165
+ this.rule = init.rule;
166
+ this.scope = init.scope;
167
+ this.qualifiedName = init.qualifiedName;
168
+ this.name = init.rule.name;
169
+ this.keyword = normalizeKeyword(init.rule.keyword ?? "Rule");
170
+ if (init.rule.background) {
171
+ this.background = init.rule.background;
172
+ }
173
+ this.scenarios = init.scenarios;
174
+ this.scenarioOutlines = init.outlines;
150
175
  }
151
176
  };
152
- var ScenarioOutlineBridge = class extends GherkinCodeBridge {
153
- constructor() {
154
- super(...arguments);
155
- this.examples = [];
156
- this.steps = [];
157
- }
158
- get title() {
159
- return this.data.scope.title(this.data.gherkin);
177
+ var ScenarioExecutionBase = class {
178
+ constructor(type, init) {
179
+ this.resultState = { status: "pending" };
180
+ this.type = type;
181
+ this.id = init.id;
182
+ this.name = init.name;
183
+ this.keyword = normalizeKeyword(init.keyword);
184
+ this.qualifiedName = init.qualifiedName;
185
+ this.tags = [...init.tags];
186
+ this.mode = init.mode;
187
+ this.pending = init.pending;
188
+ if (init.pendingReason !== void 0) {
189
+ this.pendingReason = init.pendingReason;
190
+ }
191
+ if (init.timeout !== void 0) {
192
+ this.timeout = init.timeout;
193
+ }
194
+ const data = cloneData(init.data);
195
+ if (data !== void 0) {
196
+ this.data = data;
197
+ }
198
+ this.feature = init.feature;
199
+ if (init.rule) {
200
+ this.rule = init.rule;
201
+ }
202
+ if (init.outline) {
203
+ this.outline = init.outline;
204
+ }
205
+ this.scope = init.scope;
206
+ this.summary = init.summary;
207
+ this.gherkin = init.gherkin;
208
+ this.gherkinSteps = [...init.gherkinSteps];
209
+ this.steps = [...init.steps];
210
+ this.ancestors = [...init.ancestors];
211
+ }
212
+ get result() {
213
+ return this.resultState;
214
+ }
215
+ markPassed() {
216
+ const startedAt = this.resultState.startedAt ?? Date.now();
217
+ this.resultState = {
218
+ status: "passed",
219
+ startedAt,
220
+ completedAt: Date.now()
221
+ };
160
222
  }
161
- get tags() {
162
- return [...this.data.gherkin.tags];
223
+ markFailed(error) {
224
+ const startedAt = this.resultState.startedAt ?? Date.now();
225
+ this.resultState = {
226
+ status: "failed",
227
+ error: normalizeError(error),
228
+ startedAt,
229
+ completedAt: Date.now()
230
+ };
163
231
  }
164
- accumulateTags() {
165
- const exampleTags = this.examples.map((example) => example.tags).flat();
166
- const allTags = [...this.data.gherkin.tags, ...exampleTags];
167
- return [...new Set(allTags)];
232
+ markSkipped(reason) {
233
+ const timestamp = Date.now();
234
+ this.resultState = {
235
+ status: "skipped",
236
+ startedAt: this.resultState.startedAt ?? timestamp,
237
+ completedAt: timestamp,
238
+ ...reason !== void 0 ? { reason } : {}
239
+ };
168
240
  }
169
- };
170
- var ExamplesBridge = class extends GherkinCodeBridge {
171
- constructor() {
172
- super(...arguments);
173
- this.scenarios = [];
174
- this.steps = [];
241
+ markPending(reason) {
242
+ const timestamp = Date.now();
243
+ this.resultState = {
244
+ status: "pending",
245
+ startedAt: this.resultState.startedAt ?? timestamp,
246
+ completedAt: timestamp,
247
+ ...reason !== void 0 ? { reason } : {}
248
+ };
175
249
  }
176
- get title() {
177
- return this.data.scope.title(this.data.gherkin);
250
+ reset() {
251
+ this.resultState = { status: "pending" };
178
252
  }
179
- get tags() {
180
- return [...this.data.gherkin.tags];
253
+ };
254
+ var ScenarioNodeImpl = class extends ScenarioExecutionBase {
255
+ constructor(init) {
256
+ super("scenario", init);
181
257
  }
182
- accumulateTags() {
183
- const scenarioTags = this.scenarios.map((scenario) => scenario.accumulateTags()).flat();
184
- const allTags = [...this.data.gherkin.tags, ...scenarioTags];
185
- return [...new Set(allTags)];
258
+ };
259
+ var ScenarioOutlineExampleImpl = class extends ScenarioExecutionBase {
260
+ constructor(init) {
261
+ super("example", init);
262
+ this.outline = init.outline;
263
+ this.exampleGroup = init.exampleGroup;
264
+ this.compiled = init.gherkin;
265
+ this.exampleIndex = init.exampleIndex;
186
266
  }
187
267
  };
188
- var StepBridge = class extends GherkinCodeBridge {
189
- get args() {
190
- return this.data.args;
268
+ var ScenarioOutlineNodeImpl = class {
269
+ constructor(init) {
270
+ this.type = "scenarioOutline";
271
+ this.outline = init.outline;
272
+ this.summary = init.summary;
273
+ this.scope = init.scope;
274
+ this.keyword = normalizeKeyword(init.keyword);
275
+ this.name = init.name;
276
+ this.qualifiedName = init.qualifiedName;
277
+ this.tags = [...init.tags];
278
+ this.mode = init.mode;
279
+ this.pending = init.pending;
280
+ if (init.pendingReason !== void 0) {
281
+ this.pendingReason = init.pendingReason;
282
+ }
283
+ if (init.timeout !== void 0) {
284
+ this.timeout = init.timeout;
285
+ }
286
+ const data = cloneData(init.data);
287
+ if (data !== void 0) {
288
+ this.data = data;
289
+ }
290
+ this.ancestors = [...init.ancestors];
291
+ if (init.rule) {
292
+ this.rule = init.rule;
293
+ }
294
+ this.feature = init.feature;
295
+ this.mutableExamples = init.examples;
191
296
  }
192
- get expressionText() {
193
- return this.data.scope.expression.source;
297
+ get examples() {
298
+ return this.mutableExamples;
194
299
  }
195
300
  };
196
301
 
197
- // src/bridges/bridge-search.ts
198
- var import_errors = require("@autometa/errors");
199
- function find(bridge, testName) {
200
- if (bridge instanceof FeatureBridge) {
201
- return findByFeature(bridge, testName);
202
- }
203
- if (bridge instanceof RuleBridge) {
204
- return findByRule(bridge, testName);
302
+ // src/internal/summaries.ts
303
+ function bucketScenarioSummaries(summaries) {
304
+ const buckets = /* @__PURE__ */ new Map();
305
+ for (const summary of summaries) {
306
+ const key = createSummaryKey(
307
+ summary.scenario.kind,
308
+ summary.scenario.name,
309
+ summary.rule?.id
310
+ );
311
+ const existing = buckets.get(key) ?? [];
312
+ existing.push(summary);
313
+ buckets.set(key, existing);
205
314
  }
206
- if (bridge instanceof ScenarioOutlineBridge) {
207
- return findScenarioOutlineOrChild(bridge, testName);
315
+ return buckets;
316
+ }
317
+ function createSummaryKey(kind, scenarioName, parentScopeId) {
318
+ return [
319
+ kind,
320
+ normalizeName(scenarioName),
321
+ parentScopeId ?? "root"
322
+ ].join("::");
323
+ }
324
+ function describeSummary(summary) {
325
+ const parts = [summary.scenario.name];
326
+ if (summary.rule) {
327
+ parts.push(`in rule '${summary.rule.name}'`);
208
328
  }
209
- throw new import_errors.AutomationError(`Could not find test matching ${testName}`);
329
+ return `${summary.scenario.kind} '${parts.join(" ")}'`;
210
330
  }
211
- function findByFeature(feature, testName) {
212
- const title = feature.data.scope.title(feature.data.gherkin);
213
- const byScenario = findTestTypes(feature.scenarios, testName, title);
214
- if (byScenario) {
215
- return byScenario;
331
+
332
+ // src/internal/scope-resolution.ts
333
+ function resolveFeatureScope(adapter, feature) {
334
+ const normalizedName = normalizeName(feature.name);
335
+ const featureUri = feature.uri ? normalizeUri(feature.uri) : void 0;
336
+ const matches = adapter.features.filter((scope) => {
337
+ if (scope.kind !== "feature") {
338
+ return false;
339
+ }
340
+ const scopeName = normalizeName(scope.name);
341
+ if (scopeName !== normalizedName) {
342
+ return false;
343
+ }
344
+ if (!featureUri) {
345
+ return true;
346
+ }
347
+ const scopeFile = typeof scope.data?.file === "string" ? normalizeUri(scope.data.file) : void 0;
348
+ return scopeFile ? scopeFile === featureUri : true;
349
+ });
350
+ if (matches.length === 1) {
351
+ const [match] = matches;
352
+ if (!match) {
353
+ throw new Error("Unexpected empty feature scope match");
354
+ }
355
+ return match;
216
356
  }
217
- const byRule = findRuleTypes(feature.rules, testName, title);
218
- if (byRule) {
219
- return byRule;
357
+ if (matches.length === 0) {
358
+ throw new Error(
359
+ `No feature scope registered for feature '${feature.name}'. Provide featureScope explicitly to resolve the association.`
360
+ );
220
361
  }
362
+ throw new Error(
363
+ `Multiple feature scopes match feature '${feature.name}'. Provide featureScope explicitly to disambiguate.`
364
+ );
221
365
  }
222
- function findByRule(rule, testName) {
223
- const title = rule.data.scope.title(rule.data.gherkin);
224
- const byScenario = findTestTypes(rule.scenarios, testName, title);
225
- if (byScenario) {
226
- return byScenario;
366
+ function findChildScope(parent, kind, name) {
367
+ const normalizedName = normalizeName(name);
368
+ const matches = parent.children.filter(
369
+ (child) => child.kind === kind && normalizeName(child.name) === normalizedName
370
+ );
371
+ if (matches.length === 1) {
372
+ const [match] = matches;
373
+ if (!match) {
374
+ throw new Error("Unexpected empty child scope match");
375
+ }
376
+ return match;
227
377
  }
228
- const byRule = findRuleOrChild(rule, testName, title);
229
- if (byRule) {
230
- return byRule;
378
+ if (matches.length === 0) {
379
+ throw new Error(
380
+ `Could not find ${kind} scope named '${name}' beneath '${parent.name}'`
381
+ );
231
382
  }
383
+ throw new Error(
384
+ `Multiple ${kind} scopes named '${name}' were found beneath '${parent.name}'. Names must be unique within the same parent scope.`
385
+ );
232
386
  }
233
- function findTestTypes(scenarios, testName, from) {
234
- for (const scenario of scenarios) {
235
- if (scenario instanceof ScenarioOutlineBridge) {
236
- const found = findScenarioOutlineOrChild(scenario, testName, from);
237
- if (found) {
238
- return found;
387
+
388
+ // src/internal/test-plan-builder.ts
389
+ var FEATURE_SEGMENT_KEY = "feature";
390
+ var RULE_SEGMENT_KEY = "rule";
391
+ var EXAMPLE_SEGMENT_KEY = "Example";
392
+ var _TestPlanBuilder = class _TestPlanBuilder {
393
+ constructor(feature, featureScope, adapter) {
394
+ this.feature = feature;
395
+ this.featureScope = featureScope;
396
+ this.adapter = adapter;
397
+ this.executions = [];
398
+ this.byId = /* @__PURE__ */ new Map();
399
+ this.byQualifiedName = /* @__PURE__ */ new Map();
400
+ this.featureScenarios = [];
401
+ this.featureScenarioOutlines = [];
402
+ this.featureRules = [];
403
+ this.ruleScenarioMap = /* @__PURE__ */ new Map();
404
+ this.ruleOutlineMap = /* @__PURE__ */ new Map();
405
+ this.outlineExamplesMap = /* @__PURE__ */ new Map();
406
+ const summaries = adapter.listScenarios().filter((summary) => summary.feature.id === featureScope.id);
407
+ this.summaryBuckets = bucketScenarioSummaries(summaries);
408
+ this.featureSegment = {
409
+ keyword: normalizeKeyword(feature.keyword ?? FEATURE_SEGMENT_KEY),
410
+ name: feature.name,
411
+ suffix: buildScopeSuffix(featureScope.id)
412
+ };
413
+ this.parameterRegistry = resolveParameterRegistry(adapter.getParameterRegistry?.());
414
+ createDefaultParameterTypes()(this.parameterRegistry);
415
+ }
416
+ build() {
417
+ this.featureNode = createFeatureNode({
418
+ feature: this.feature,
419
+ scope: this.featureScope,
420
+ executions: this.executions,
421
+ scenarios: this.featureScenarios,
422
+ outlines: this.featureScenarioOutlines,
423
+ rules: this.featureRules
424
+ });
425
+ for (const element of this.feature.elements ?? []) {
426
+ if (isRule(element)) {
427
+ this.processRule(element);
428
+ } else if (isScenarioOutline(element)) {
429
+ this.processScenarioOutline(element, void 0);
430
+ } else if (isScenario(element)) {
431
+ this.processScenario(element, void 0);
239
432
  }
240
433
  }
241
- if (scenario instanceof ExamplesBridge) {
242
- const found = findExamplesOrChild(scenario, testName, from);
243
- if (found) {
244
- return found;
434
+ this.ensureAllSummariesConsumed();
435
+ return new TestPlanImpl(
436
+ this.featureNode,
437
+ this.executions,
438
+ this.byId,
439
+ this.byQualifiedName
440
+ );
441
+ }
442
+ processRule(rule) {
443
+ const ruleScope = findChildScope(this.featureScope, "rule", rule.name);
444
+ const qualifiedName = buildQualifiedName([
445
+ this.featureSegment,
446
+ {
447
+ keyword: normalizeKeyword(rule.keyword ?? RULE_SEGMENT_KEY),
448
+ name: rule.name,
449
+ suffix: buildScopeSuffix(ruleScope.id)
245
450
  }
246
- }
247
- if (scenario instanceof ScenarioBridge) {
248
- const found = findScenario(scenario, testName, from);
249
- if (found) {
250
- return found;
451
+ ]);
452
+ const ruleScenarios = [];
453
+ const ruleOutlines = [];
454
+ const ruleNode = createRuleNode({
455
+ rule,
456
+ scope: ruleScope,
457
+ qualifiedName,
458
+ scenarios: ruleScenarios,
459
+ outlines: ruleOutlines
460
+ });
461
+ this.ruleScenarioMap.set(ruleNode, ruleScenarios);
462
+ this.ruleOutlineMap.set(ruleNode, ruleOutlines);
463
+ this.featureRules.push(ruleNode);
464
+ for (const element of rule.elements ?? []) {
465
+ if (isScenarioOutline(element)) {
466
+ this.processScenarioOutline(element, ruleNode);
467
+ } else if (isScenario(element)) {
468
+ this.processScenario(element, ruleNode);
251
469
  }
252
470
  }
253
471
  }
254
- }
255
- function findRuleTypes(rules, testName, from) {
256
- for (const rule of rules) {
257
- const found = findRuleOrChild(rule, testName, from);
258
- if (found) {
259
- return found;
472
+ processScenario(gherkinScenario, ruleNode) {
473
+ const ruleScope = ruleNode?.scope;
474
+ const summary = this.consumeSummary(
475
+ "scenario",
476
+ gherkinScenario.name,
477
+ ruleScope
478
+ );
479
+ if (summary.scenario.kind !== "scenario") {
480
+ throw new Error(
481
+ `Scope mismatch: expected scenario kind 'scenario' but received '${summary.scenario.kind}' for '${gherkinScenario.name}'`
482
+ );
483
+ }
484
+ const qualifiedName = buildQualifiedName([
485
+ this.featureSegment,
486
+ ...ruleNode ? [
487
+ {
488
+ keyword: normalizeKeyword(ruleNode.keyword),
489
+ name: ruleNode.name,
490
+ suffix: buildScopeSuffix(ruleNode.scope.id)
491
+ }
492
+ ] : [],
493
+ {
494
+ keyword: normalizeKeyword(gherkinScenario.keyword ?? "Scenario"),
495
+ name: gherkinScenario.name,
496
+ suffix: buildScopeSuffix(summary.scenario.id)
497
+ }
498
+ ]);
499
+ const gherkinSteps = combineSteps(
500
+ this.feature.background?.steps,
501
+ ruleNode?.background?.steps,
502
+ gherkinScenario.steps
503
+ );
504
+ const resolvedSteps = this.resolveStepDefinitions(summary, gherkinSteps, {
505
+ scenario: gherkinScenario.name,
506
+ ...ruleNode ? { rule: ruleNode.name } : {}
507
+ });
508
+ const scenarioData = cloneData(summary.scenario.data);
509
+ const scenarioNode = createScenarioNode({
510
+ id: summary.id,
511
+ feature: this.featureNode,
512
+ name: gherkinScenario.name,
513
+ keyword: gherkinScenario.keyword ?? "Scenario",
514
+ qualifiedName,
515
+ tags: collectTags(
516
+ this.feature.tags,
517
+ ruleNode?.rule.tags,
518
+ gherkinScenario.tags,
519
+ summary.feature.tags,
520
+ summary.scenario.tags
521
+ ),
522
+ mode: summary.scenario.mode,
523
+ pending: summary.scenario.pending,
524
+ ...summary.scenario.pendingReason ? { pendingReason: summary.scenario.pendingReason } : {},
525
+ scope: summary.scenario,
526
+ summary,
527
+ gherkin: gherkinScenario,
528
+ gherkinSteps,
529
+ steps: resolvedSteps,
530
+ ancestors: summary.ancestors,
531
+ ...ruleNode ? { rule: ruleNode } : {},
532
+ ...summary.scenario.timeout !== void 0 ? { timeout: summary.scenario.timeout } : {},
533
+ ...scenarioData ? { data: scenarioData } : {}
534
+ });
535
+ if (ruleNode) {
536
+ this.getRuleScenarios(ruleNode).push(scenarioNode);
537
+ } else {
538
+ this.featureScenarios.push(scenarioNode);
539
+ }
540
+ this.registerExecution(scenarioNode);
541
+ }
542
+ processScenarioOutline(outline, ruleNode) {
543
+ const ruleScope = ruleNode?.scope;
544
+ const summary = this.consumeSummary(
545
+ "scenarioOutline",
546
+ outline.name,
547
+ ruleScope
548
+ );
549
+ if (summary.scenario.kind !== "scenarioOutline") {
550
+ throw new Error(
551
+ `Scope mismatch: expected scenario kind 'scenarioOutline' but received '${summary.scenario.kind}' for '${outline.name}'`
552
+ );
553
+ }
554
+ const outlineQualifiedName = buildQualifiedName([
555
+ this.featureSegment,
556
+ ...ruleNode ? [
557
+ {
558
+ keyword: normalizeKeyword(ruleNode.keyword),
559
+ name: ruleNode.name,
560
+ suffix: buildScopeSuffix(ruleNode.scope.id)
561
+ }
562
+ ] : [],
563
+ {
564
+ keyword: normalizeKeyword(outline.keyword ?? "Scenario Outline"),
565
+ name: outline.name,
566
+ suffix: buildScopeSuffix(summary.scenario.id)
567
+ }
568
+ ]);
569
+ const outlineData = cloneData(summary.scenario.data);
570
+ const outlineExamples = [];
571
+ const outlineNode = createScenarioOutlineNode({
572
+ outline,
573
+ summary,
574
+ scope: summary.scenario,
575
+ keyword: outline.keyword ?? "Scenario Outline",
576
+ name: outline.name,
577
+ qualifiedName: outlineQualifiedName,
578
+ tags: collectTags(
579
+ this.feature.tags,
580
+ ruleNode?.rule.tags,
581
+ outline.tags,
582
+ summary.feature.tags,
583
+ summary.scenario.tags
584
+ ),
585
+ mode: summary.scenario.mode,
586
+ pending: summary.scenario.pending,
587
+ ...summary.scenario.pendingReason ? { pendingReason: summary.scenario.pendingReason } : {},
588
+ ancestors: summary.ancestors,
589
+ feature: this.featureNode,
590
+ ...summary.scenario.timeout !== void 0 ? { timeout: summary.scenario.timeout } : {},
591
+ ...outlineData ? { data: outlineData } : {},
592
+ ...ruleNode ? { rule: ruleNode } : {},
593
+ examples: outlineExamples
594
+ });
595
+ this.outlineExamplesMap.set(outlineNode, outlineExamples);
596
+ if (ruleNode) {
597
+ this.getRuleOutlines(ruleNode).push(outlineNode);
598
+ } else {
599
+ this.featureScenarioOutlines.push(outlineNode);
600
+ }
601
+ const compiledByGroup = groupCompiledScenarios(outline.compiledScenarios);
602
+ for (const group of outline.exampleGroups ?? []) {
603
+ const compiledForGroup = compiledByGroup.get(group.id) ?? [];
604
+ compiledForGroup.sort((a, b) => a.exampleIndex - b.exampleIndex);
605
+ for (const compiled of compiledForGroup) {
606
+ const qualifiedName = buildQualifiedName([
607
+ this.featureSegment,
608
+ ...ruleNode ? [
609
+ {
610
+ keyword: normalizeKeyword(ruleNode.keyword),
611
+ name: ruleNode.name,
612
+ suffix: buildScopeSuffix(ruleNode.scope.id)
613
+ }
614
+ ] : [],
615
+ {
616
+ keyword: normalizeKeyword(outline.keyword ?? "Scenario Outline"),
617
+ name: outline.name,
618
+ suffix: buildScopeSuffix(summary.scenario.id)
619
+ },
620
+ {
621
+ keyword: EXAMPLE_SEGMENT_KEY,
622
+ name: compiled.name,
623
+ suffix: buildExampleSuffix(compiled.id, compiled.exampleIndex)
624
+ }
625
+ ]);
626
+ const gherkinSteps = combineSteps(
627
+ this.feature.background?.steps,
628
+ ruleNode?.background?.steps,
629
+ compiled.steps
630
+ );
631
+ const resolvedSteps = this.resolveStepDefinitions(summary, gherkinSteps, {
632
+ scenario: compiled.name,
633
+ outline: outline.name,
634
+ ...ruleNode ? { rule: ruleNode.name } : {}
635
+ });
636
+ const exampleData = mergeData(
637
+ cloneData(summary.scenario.data),
638
+ createExampleData(group, compiled)
639
+ );
640
+ const exampleExecution = createScenarioOutlineExample({
641
+ id: compiled.id,
642
+ feature: this.featureNode,
643
+ outline: outlineNode,
644
+ name: compiled.name,
645
+ keyword: compiled.keyword ?? outline.keyword ?? "Scenario Outline",
646
+ qualifiedName,
647
+ tags: collectTags(
648
+ this.feature.tags,
649
+ ruleNode?.rule.tags,
650
+ outline.tags,
651
+ group.tags,
652
+ compiled.tags,
653
+ summary.feature.tags,
654
+ summary.scenario.tags
655
+ ),
656
+ mode: summary.scenario.mode,
657
+ pending: summary.scenario.pending,
658
+ ...summary.scenario.pendingReason ? { pendingReason: summary.scenario.pendingReason } : {},
659
+ scope: summary.scenario,
660
+ summary,
661
+ gherkin: compiled,
662
+ gherkinSteps,
663
+ steps: resolvedSteps,
664
+ ancestors: summary.ancestors,
665
+ exampleGroup: group,
666
+ exampleIndex: compiled.exampleIndex,
667
+ ...ruleNode ? { rule: ruleNode } : {},
668
+ ...summary.scenario.timeout !== void 0 ? { timeout: summary.scenario.timeout } : {},
669
+ ...exampleData ? { data: exampleData } : {}
670
+ });
671
+ this.getOutlineExamples(outlineNode).push(exampleExecution);
672
+ this.registerExecution(exampleExecution);
673
+ }
260
674
  }
261
675
  }
262
- }
263
- function findRuleOrChild(rule, testName, from) {
264
- const {
265
- data: { scope: scope2, gherkin }
266
- } = rule;
267
- const title = scope2.title(gherkin);
268
- if (testName === title) {
269
- return rule;
676
+ consumeSummary(kind, scenarioName, parentScope) {
677
+ const key = createSummaryKey(kind, scenarioName, parentScope?.id);
678
+ const bucket = this.summaryBuckets.get(key);
679
+ if (!bucket || bucket.length === 0) {
680
+ throw new Error(
681
+ `Could not find a registered ${kind} scope for '${scenarioName}'${parentScope ? ` within '${parentScope.name}'` : ""}`
682
+ );
683
+ }
684
+ const summary = bucket.shift();
685
+ if (bucket.length === 0) {
686
+ this.summaryBuckets.delete(key);
687
+ }
688
+ return summary;
270
689
  }
271
- if (from) {
272
- const fullTitle = `${from} ${title}`;
273
- if (fullTitle === testName) {
274
- return rule;
690
+ registerExecution(execution) {
691
+ if (this.byId.has(execution.id)) {
692
+ throw new Error(
693
+ `Duplicate scenario identifier detected: '${execution.id}' for '${execution.qualifiedName}'`
694
+ );
695
+ }
696
+ if (this.byQualifiedName.has(execution.qualifiedName)) {
697
+ throw new Error(
698
+ `Duplicate qualified scenario name detected: '${execution.qualifiedName}'`
699
+ );
700
+ }
701
+ this.executions.push(execution);
702
+ this.byId.set(execution.id, execution);
703
+ this.byQualifiedName.set(execution.qualifiedName, execution);
704
+ }
705
+ ensureAllSummariesConsumed() {
706
+ const leftovers = [];
707
+ for (const bucket of this.summaryBuckets.values()) {
708
+ for (const summary of bucket) {
709
+ leftovers.push(describeSummary(summary));
710
+ }
711
+ }
712
+ if (leftovers.length > 0) {
713
+ throw new Error(
714
+ `The following scope scenarios were not matched to Gherkin nodes for feature '${this.feature.name}': ${leftovers.join(", ")}`
715
+ );
275
716
  }
276
717
  }
277
- const newFrom = appendPath(from, title);
278
- return findTestTypes(rule.scenarios, testName, newFrom);
279
- }
280
- function findScenarioOutlineOrChild(outline, testName, from) {
281
- const {
282
- data: { scope: scope2, gherkin }
283
- } = outline;
284
- const title = scope2.title(gherkin);
285
- if (testName === title) {
286
- return outline;
718
+ getRuleScenarios(rule) {
719
+ const list = this.ruleScenarioMap.get(rule);
720
+ if (!list) {
721
+ throw new Error(`Rule '${rule.name}' has no associated scenario registry`);
722
+ }
723
+ return list;
287
724
  }
288
- if (from) {
289
- const fullTitle = `${from} ${title}`;
290
- if (fullTitle === testName) {
291
- return outline;
725
+ getRuleOutlines(rule) {
726
+ const list = this.ruleOutlineMap.get(rule);
727
+ if (!list) {
728
+ throw new Error(`Rule '${rule.name}' has no associated scenario outline registry`);
292
729
  }
730
+ return list;
293
731
  }
294
- for (const example of outline.examples) {
295
- const newFrom = appendPath(from, title);
296
- const found = findExamplesOrChild(example, testName, newFrom);
297
- if (found) {
298
- return found;
732
+ getOutlineExamples(outline) {
733
+ const list = this.outlineExamplesMap.get(outline);
734
+ if (!list) {
735
+ throw new Error(
736
+ `Scenario outline '${outline.name}' has no associated example registry`
737
+ );
299
738
  }
739
+ return list;
300
740
  }
301
- }
302
- function appendPath(from, title) {
303
- return from ? `${from} ${title}` : title;
304
- }
305
- function findExamplesOrChild(example, testName, from) {
306
- const {
307
- data: { gherkin }
308
- } = example;
309
- const title = `${gherkin.keyword}: ${gherkin.name}`;
310
- if (testName === title) {
311
- return example;
741
+ resolveStepDefinitions(summary, gherkinSteps, context) {
742
+ if (summary.steps.length === 0) {
743
+ if (gherkinSteps.length === 0) {
744
+ return [];
745
+ }
746
+ return gherkinSteps.map(
747
+ (step, index) => this.createMissingStepDefinition(summary, context, step, index)
748
+ );
749
+ }
750
+ const remaining = new Set(summary.steps);
751
+ const ordered = [];
752
+ const matchers = /* @__PURE__ */ new Map();
753
+ let encounteredMissing = false;
754
+ for (const step of gherkinSteps) {
755
+ const matched = this.findMatchingStepDefinition(
756
+ step,
757
+ summary.steps,
758
+ remaining,
759
+ matchers
760
+ );
761
+ if (!matched) {
762
+ ordered.push(
763
+ this.createMissingStepDefinition(
764
+ summary,
765
+ context,
766
+ step,
767
+ ordered.length
768
+ )
769
+ );
770
+ encounteredMissing = true;
771
+ continue;
772
+ }
773
+ ordered.push(matched);
774
+ remaining.delete(matched);
775
+ }
776
+ if (encounteredMissing) {
777
+ return ordered;
778
+ }
779
+ return ordered;
780
+ }
781
+ findMatchingStepDefinition(step, definitions, remaining, matchers) {
782
+ const rawKeyword = normalizeKeyword(step.keyword ?? "");
783
+ const wildcard = isFlexibleKeyword(rawKeyword);
784
+ const keyword = wildcard ? void 0 : normalizeGherkinStepKeyword(rawKeyword);
785
+ const candidates = definitions.filter((definition) => {
786
+ if (!remaining.has(definition)) {
787
+ return false;
788
+ }
789
+ if (wildcard) {
790
+ return true;
791
+ }
792
+ return keyword ? definition.keyword === keyword : false;
793
+ });
794
+ for (const definition of candidates) {
795
+ if (this.matchesStepExpression(definition, step.text, matchers)) {
796
+ return definition;
797
+ }
798
+ }
799
+ return void 0;
312
800
  }
313
- if (from) {
314
- const fullTitle = `${from} ${title}`;
315
- if (fullTitle === testName) {
316
- return example;
801
+ matchesStepExpression(definition, text, matchers) {
802
+ let matcher = matchers.get(definition);
803
+ if (!matcher) {
804
+ matcher = this.createMatcher(definition.expression);
805
+ matchers.set(definition, matcher);
317
806
  }
807
+ return matcher(text);
318
808
  }
319
- for (const scenario of example.scenarios) {
320
- const newFrom = appendPath(from, title);
321
- const found = findScenario(scenario, testName, newFrom);
322
- if (found) {
323
- return found;
809
+ createMatcher(expression) {
810
+ if (expression instanceof RegExp) {
811
+ const regex = new RegExp(expression.source, expression.flags);
812
+ const evaluator = new RegularExpression(regex, this.parameterRegistry);
813
+ return (text) => evaluator.match(text) !== null;
814
+ }
815
+ try {
816
+ const cucumberExpression = new CucumberExpression(
817
+ expression,
818
+ this.parameterRegistry
819
+ );
820
+ return (text) => cucumberExpression.match(text) !== null;
821
+ } catch {
822
+ const literal = expression;
823
+ return (text) => text === literal;
324
824
  }
325
825
  }
326
- }
327
- function findScenario(scenario, testName, from) {
328
- const {
329
- data: { scope: scope2, gherkin }
330
- } = scenario;
331
- const title = scope2.title(gherkin);
332
- if (testName === title) {
333
- return scenario;
826
+ buildMissingStepDefinitionMessage(context, step, definitions) {
827
+ const keyword = (step.keyword ?? "").trim();
828
+ const display = keyword.length > 0 ? `${keyword} ${step.text}` : step.text;
829
+ const lines = [
830
+ "No step definition matched:",
831
+ "",
832
+ `'${display}'`,
833
+ "",
834
+ this.buildMissingStepContextLine(context)
835
+ ];
836
+ const suggestions = this.resolveClosestStepDefinitionSuggestions(step, definitions);
837
+ if (suggestions.sameType.length === 0 && suggestions.differentType.length === 0) {
838
+ return lines.join("\n");
839
+ }
840
+ lines.push("", "Some close matches were found:");
841
+ if (suggestions.sameType.length > 0) {
842
+ lines.push(" Close matches with the same step type:");
843
+ for (const suggestion of suggestions.sameType) {
844
+ lines.push(` - ${suggestion}`);
845
+ }
846
+ }
847
+ if (suggestions.differentType.length > 0) {
848
+ lines.push(" Close matches with different step type:");
849
+ for (const suggestion of suggestions.differentType) {
850
+ lines.push(` - ${suggestion}`);
851
+ }
852
+ }
853
+ return lines.join("\n");
334
854
  }
335
- if (from) {
336
- const fullTitle = `${from} ${title}`;
337
- if (fullTitle === testName) {
338
- return scenario;
855
+ buildMissingStepContextLine(context) {
856
+ const parts = [`in scenario '${context.scenario}'`];
857
+ if (context.outline) {
858
+ parts.push(`of outline '${context.outline}'`);
859
+ }
860
+ if (context.rule) {
861
+ parts.push(`within rule '${context.rule}'`);
339
862
  }
863
+ parts.push(`for feature '${this.feature.name}'.`);
864
+ return parts.join(" ");
865
+ }
866
+ buildUnusedStepDefinitionsMessage(context, extras) {
867
+ const segments = [
868
+ `The following step definitions were not matched to Gherkin steps in scenario '${context.scenario}'`
869
+ ];
870
+ if (context.outline) {
871
+ segments.push(`of outline '${context.outline}'`);
872
+ }
873
+ if (context.rule) {
874
+ segments.push(`within rule '${context.rule}'`);
875
+ }
876
+ segments.push(`for feature '${this.feature.name}': ${extras.join(", ")}`);
877
+ return segments.join(" ");
340
878
  }
341
- }
342
-
343
- // src/bridges/bridge-query.ts
344
- var import_gherkin2 = require("@autometa/gherkin");
345
- function failed(bridge) {
346
- const accumulator = [];
347
- if (bridge instanceof ScenarioOutlineBridge) {
348
- return failedOutline(bridge);
349
- }
350
- for (const scenario of bridge.scenarios) {
351
- if (scenario instanceof ScenarioOutlineBridge) {
352
- accumulator.push(...failedOutline(scenario));
353
- } else if (scenario.report.error !== void 0) {
354
- accumulator.push(scenario);
355
- }
356
- }
357
- if (bridge instanceof FeatureBridge) {
358
- accumulator.push(...failedRule(bridge));
359
- }
360
- return accumulator;
361
- }
362
- function failedOutline(bridge) {
363
- const accumulator = [];
364
- for (const example of bridge.examples) {
365
- for (const scenario of example.scenarios) {
366
- if (scenario.report.error) {
367
- accumulator.push(scenario);
368
- }
879
+ describeStepDefinition(definition) {
880
+ return `${definition.keyword} ${formatExpression(definition.expression)}`;
881
+ }
882
+ createMissingStepDefinition(summary, context, step, index) {
883
+ const message = this.buildMissingStepDefinitionMessage(
884
+ context,
885
+ step,
886
+ summary.steps
887
+ );
888
+ return {
889
+ id: `${summary.id}:missing-step:${index}`,
890
+ keyword: this.resolveStepKeyword(step),
891
+ expression: step.text,
892
+ handler: () => {
893
+ throw new Error(message);
894
+ },
895
+ options: this.createFallbackStepOptions(summary.scenario.mode)
896
+ };
897
+ }
898
+ resolveStepKeyword(step) {
899
+ const raw = normalizeKeyword(step.keyword ?? "");
900
+ if (isFlexibleKeyword(raw)) {
901
+ return "And";
902
+ }
903
+ try {
904
+ return normalizeGherkinStepKeyword(raw);
905
+ } catch {
906
+ return "Given";
369
907
  }
370
908
  }
371
- return accumulator;
372
- }
373
- function failedRule(bridge) {
374
- const accumulator = [];
375
- for (const rule of bridge.rules) {
376
- for (const scenario of rule.scenarios) {
377
- if (scenario instanceof ScenarioOutlineBridge) {
378
- accumulator.push(...failedOutline(scenario));
379
- } else if (scenario.report.error) {
380
- accumulator.push(scenario);
909
+ createFallbackStepOptions(mode) {
910
+ return {
911
+ tags: [],
912
+ mode
913
+ };
914
+ }
915
+ resolveClosestStepDefinitionSuggestions(step, definitions) {
916
+ if (definitions.length === 0) {
917
+ return { sameType: [], differentType: [] };
918
+ }
919
+ const desiredKeyword = this.tryNormalizeKeyword(step.keyword);
920
+ const target = this.normalizeForDistance(step.text);
921
+ const candidates = definitions.map((definition) => {
922
+ const candidateText = this.normalizeForDistance(
923
+ formatExpression(definition.expression)
924
+ );
925
+ const distance = this.computeEditDistance(target, candidateText);
926
+ const similarity = this.computeSimilarity(target, candidateText, distance);
927
+ return {
928
+ definition,
929
+ description: this.describeStepDefinition(definition),
930
+ distance,
931
+ similarity
932
+ };
933
+ });
934
+ candidates.sort((a, b) => a.distance - b.distance);
935
+ const sameType = [];
936
+ const differentType = [];
937
+ for (const candidate of candidates) {
938
+ const isSameType = !desiredKeyword || candidate.definition.keyword === desiredKeyword;
939
+ if (isSameType) {
940
+ if (candidate.similarity < _TestPlanBuilder.SUGGESTION_MIN_SIMILARITY_SAME_TYPE) {
941
+ continue;
942
+ }
943
+ if (sameType.length < _TestPlanBuilder.SUGGESTION_LIMIT_PER_GROUP) {
944
+ sameType.push(candidate.description);
945
+ }
946
+ continue;
947
+ }
948
+ if (candidate.similarity < _TestPlanBuilder.SUGGESTION_MIN_SIMILARITY_DIFFERENT_TYPE) {
949
+ continue;
950
+ }
951
+ if (differentType.length < _TestPlanBuilder.SUGGESTION_LIMIT_PER_GROUP) {
952
+ differentType.push(candidate.description);
953
+ }
954
+ if (sameType.length >= _TestPlanBuilder.SUGGESTION_LIMIT_PER_GROUP && differentType.length >= _TestPlanBuilder.SUGGESTION_LIMIT_PER_GROUP) {
955
+ break;
381
956
  }
382
957
  }
958
+ return { sameType, differentType };
383
959
  }
384
- return accumulator;
385
- }
386
- function gherkinToTestNames(node, path = "", accumulator = []) {
387
- if (!("name" in node) || node instanceof import_gherkin2.Background) {
388
- return;
960
+ normalizeForDistance(value) {
961
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
389
962
  }
390
- const title = `${node.keyword}: ${node.name}`;
391
- const fullPath = path ? `${path} ${title}` : title;
392
- accumulator.push(fullPath);
393
- if (!node.children) {
394
- return;
963
+ computeSimilarity(a, b, distance) {
964
+ const maxLen = Math.max(a.length, b.length);
965
+ if (maxLen === 0) {
966
+ return 1;
967
+ }
968
+ const normalized = 1 - distance / maxLen;
969
+ return Math.max(0, Math.min(1, normalized));
395
970
  }
396
- for (const child of node.children) {
397
- if (!("name" in child) || child instanceof import_gherkin2.Background) {
398
- continue;
971
+ computeEditDistance(a, b) {
972
+ if (a === b) {
973
+ return 0;
974
+ }
975
+ const rows = a.length + 1;
976
+ const cols = b.length + 1;
977
+ const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
978
+ for (let i = 0; i < rows; i++) {
979
+ const row = matrix[i];
980
+ if (!row) {
981
+ throw new Error("Internal error: matrix row missing");
982
+ }
983
+ row[0] = i;
984
+ }
985
+ const row0 = matrix[0];
986
+ if (!row0) {
987
+ throw new Error("Internal error: matrix[0] missing");
988
+ }
989
+ for (let j = 0; j < cols; j++) {
990
+ row0[j] = j;
991
+ }
992
+ for (let i = 1; i < rows; i++) {
993
+ const row = matrix[i];
994
+ const prevRow = matrix[i - 1];
995
+ if (!row || !prevRow) {
996
+ throw new Error("Internal error: matrix row missing");
997
+ }
998
+ for (let j = 1; j < cols; j++) {
999
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1000
+ const deletion = (prevRow[j] ?? 0) + 1;
1001
+ const insertion = (row[j - 1] ?? 0) + 1;
1002
+ const substitution = (prevRow[j - 1] ?? 0) + cost;
1003
+ row[j] = Math.min(deletion, insertion, substitution);
1004
+ }
1005
+ }
1006
+ const lastRow = matrix[rows - 1];
1007
+ if (!lastRow) {
1008
+ throw new Error("Internal error: last matrix row missing");
1009
+ }
1010
+ const result = lastRow[cols - 1];
1011
+ if (result === void 0) {
1012
+ throw new Error("Internal error: matrix result missing");
399
1013
  }
400
- gherkinToTestNames(child, fullPath, accumulator);
1014
+ return result;
401
1015
  }
402
- return accumulator;
403
- }
404
- var Query = {
405
- find: {
406
- failed
407
- },
408
- testNames: gherkinToTestNames
409
- };
410
-
411
- // src/gherkin-walker.ts
412
- var import_gherkin3 = require("@autometa/gherkin");
413
- var import_errors2 = require("@autometa/errors");
414
- var import_gherkin4 = require("@autometa/gherkin");
415
- var _walkNode, walkNode_fn, _walkChildren, walkChildren_fn;
416
- var GherkinWalker = class {
417
- static walk(walkFunction, childNode, accumulator, parentNode) {
418
- if (accumulator === void 0) {
419
- throw new import_errors2.AutomationError(
420
- `An accumulator must be defined to continue the walker from ${childNode.constructor.name}${JSON.stringify(childNode)}`
421
- );
1016
+ tryNormalizeKeyword(keyword) {
1017
+ if (!keyword) {
1018
+ return void 0;
1019
+ }
1020
+ const raw = normalizeKeyword(keyword);
1021
+ if (isFlexibleKeyword(raw)) {
1022
+ return void 0;
1023
+ }
1024
+ try {
1025
+ return normalizeGherkinStepKeyword(raw);
1026
+ } catch {
1027
+ return void 0;
422
1028
  }
423
- __privateMethod(this, _walkNode, walkNode_fn).call(this, childNode, accumulator, walkFunction, parentNode);
424
- return accumulator;
425
1029
  }
426
1030
  };
427
- _walkNode = new WeakSet();
428
- walkNode_fn = function(child, accumulator, walkFunction, lastNode) {
429
- if (child instanceof import_gherkin3.Feature && walkFunction.onFeature) {
430
- const acc = walkFunction.onFeature(child, accumulator, lastNode);
431
- return __privateMethod(this, _walkChildren, walkChildren_fn).call(this, child, acc, walkFunction);
432
- }
433
- if (child instanceof import_gherkin3.Rule && walkFunction.onRule) {
434
- const acc = walkFunction.onRule(child, accumulator, lastNode);
435
- return __privateMethod(this, _walkChildren, walkChildren_fn).call(this, child, acc, walkFunction);
436
- }
437
- if (child instanceof import_gherkin3.Examples) {
438
- const acc = walkFunction.onExamples?.(child, accumulator, lastNode);
439
- return __privateMethod(this, _walkChildren, walkChildren_fn).call(this, child, acc, walkFunction);
440
- }
441
- if (child instanceof import_gherkin4.Example) {
442
- const acc = walkFunction.onExample?.(child, accumulator, lastNode);
443
- return __privateMethod(this, _walkChildren, walkChildren_fn).call(this, child, acc, walkFunction);
444
- }
445
- if (child instanceof import_gherkin3.Scenario) {
446
- const acc = walkFunction.onScenario?.(child, accumulator, lastNode);
447
- return __privateMethod(this, _walkChildren, walkChildren_fn).call(this, child, acc, walkFunction);
448
- }
449
- if (child instanceof import_gherkin3.ScenarioOutline) {
450
- const acc = walkFunction.onScenarioOutline?.(
451
- child,
452
- accumulator,
453
- lastNode
454
- );
455
- return __privateMethod(this, _walkChildren, walkChildren_fn).call(this, child, acc, walkFunction);
1031
+ _TestPlanBuilder.SUGGESTION_LIMIT_PER_GROUP = 3;
1032
+ /**
1033
+ * Similarity threshold in [0, 1].
1034
+ * 1.0 = identical, 0.0 = completely different.
1035
+ */
1036
+ _TestPlanBuilder.SUGGESTION_MIN_SIMILARITY_SAME_TYPE = 0.6;
1037
+ _TestPlanBuilder.SUGGESTION_MIN_SIMILARITY_DIFFERENT_TYPE = 0.85;
1038
+ var TestPlanBuilder = _TestPlanBuilder;
1039
+ var TestPlanImpl = class {
1040
+ constructor(feature, executions, byId, byQualifiedName) {
1041
+ this.feature = feature;
1042
+ this.executions = executions;
1043
+ this.byId = byId;
1044
+ this.byQualifiedName = byQualifiedName;
1045
+ }
1046
+ listExecutables() {
1047
+ return this.executions;
456
1048
  }
457
- if (child instanceof import_gherkin3.Background) {
458
- const acc = walkFunction?.onBackground?.(child, accumulator, lastNode);
459
- return __privateMethod(this, _walkChildren, walkChildren_fn).call(this, child, acc, walkFunction);
1049
+ listFailed() {
1050
+ return this.executions.filter((execution) => execution.result.status === "failed");
460
1051
  }
461
- if (child instanceof import_gherkin3.Step) {
462
- const acc = walkFunction.onStep?.(child, accumulator, lastNode);
463
- return __privateMethod(this, _walkChildren, walkChildren_fn).call(this, child, acc, walkFunction);
1052
+ findById(id) {
1053
+ return this.byId.get(id);
1054
+ }
1055
+ findByQualifiedName(name) {
1056
+ return this.byQualifiedName.get(name);
464
1057
  }
465
1058
  };
466
- _walkChildren = new WeakSet();
467
- walkChildren_fn = function(child, accumulator, walkFunction) {
468
- for (const node of child.children) {
469
- this.walk(walkFunction, node, accumulator, child);
1059
+ function resolveParameterRegistry(parameterRegistry) {
1060
+ if (!parameterRegistry) {
1061
+ return new ParameterTypeRegistry();
1062
+ }
1063
+ if (isParameterTypeRegistry(parameterRegistry)) {
1064
+ return parameterRegistry;
1065
+ }
1066
+ const candidate = parameterRegistry.registry;
1067
+ if (isParameterTypeRegistry(candidate)) {
1068
+ return candidate;
1069
+ }
1070
+ return new ParameterTypeRegistry();
1071
+ }
1072
+ function isParameterTypeRegistry(value) {
1073
+ if (!value || typeof value !== "object") {
1074
+ return false;
470
1075
  }
1076
+ if (value instanceof ParameterTypeRegistry) {
1077
+ return true;
1078
+ }
1079
+ const registry = value;
1080
+ return typeof registry.lookupByTypeName === "function" && typeof registry.defineParameterType === "function";
1081
+ }
1082
+ function normalizeGherkinStepKeyword(keyword) {
1083
+ const trimmed = normalizeKeyword(keyword).replace(/:$/, "");
1084
+ const mapped = STEP_KEYWORD_MAP[trimmed.toLowerCase()];
1085
+ if (!mapped) {
1086
+ throw new Error(`Unsupported Gherkin step keyword '${keyword}'`);
1087
+ }
1088
+ return mapped;
1089
+ }
1090
+ function isFlexibleKeyword(keyword) {
1091
+ const normalized = normalizeKeyword(keyword).replace(/:$/, "").toLowerCase();
1092
+ return FLEXIBLE_KEYWORDS.has(normalized);
1093
+ }
1094
+ function formatExpression(expression) {
1095
+ return typeof expression === "string" ? expression : expression.toString();
1096
+ }
1097
+ var STEP_KEYWORD_MAP = {
1098
+ given: "Given",
1099
+ when: "When",
1100
+ then: "Then",
1101
+ and: "And",
1102
+ but: "But"
471
1103
  };
472
- __privateAdd(GherkinWalker, _walkNode);
473
- __privateAdd(GherkinWalker, _walkChildren);
1104
+ var FLEXIBLE_KEYWORDS = /* @__PURE__ */ new Set(["and", "but", "*"]);
474
1105
 
475
- // src/scope-search.ts
476
- var import_errors3 = require("@autometa/errors");
477
- var import_scopes = require("@autometa/scopes");
478
- var import_scopes2 = require("@autometa/scopes");
479
- function scope(value) {
480
- return {
481
- findRule: (name) => {
482
- const found = value.closedScopes.find((child) => {
483
- return child instanceof import_scopes.RuleScope && child.name === name;
484
- });
485
- if (!found) {
486
- const rule = new import_scopes.RuleScope(
487
- name,
488
- import_scopes2.Empty_Function,
489
- void 0,
490
- value.hooks,
491
- value.steps
492
- );
493
- value.attach(rule);
494
- return rule;
495
- }
496
- return found;
497
- },
498
- findScenario: (name) => {
499
- const found = value.closedScopes.find((child) => {
500
- return child instanceof import_scopes.ScenarioScope && child.name === name && !(child instanceof import_scopes.ScenarioOutlineScope);
501
- });
502
- if (!found) {
503
- const scenario = new import_scopes.ScenarioScope(
504
- name,
505
- import_scopes2.Empty_Function,
506
- void 0,
507
- value.hooks,
508
- value.steps
509
- );
510
- value.attach(scenario);
511
- return scenario;
512
- }
513
- return found;
514
- },
515
- findBackground: ({ name }) => {
516
- const found = value.closedScopes.find((child) => {
517
- return child instanceof import_scopes.BackgroundScope;
518
- });
519
- if (found && found.name !== name) {
520
- throw new import_errors3.AutomationError(
521
- `Could not find background matching ${name} but found ${found?.name}`
522
- );
523
- }
524
- if (found) {
525
- return found;
526
- }
527
- const bgScope = new import_scopes.BackgroundScope(
528
- name,
529
- import_scopes2.Empty_Function,
530
- value.hooks,
531
- value.steps
532
- );
533
- value.attach(bgScope);
534
- return bgScope;
535
- },
536
- findScenarioOutline: (name) => {
537
- const found = value.closedScopes.find((child) => {
538
- return child instanceof import_scopes.ScenarioOutlineScope && child.name === name;
539
- });
540
- if (!found) {
541
- const scenarioOutline = new import_scopes.ScenarioOutlineScope(
542
- name,
543
- import_scopes2.Empty_Function,
544
- void 0,
545
- value.hooks,
546
- value.steps
547
- );
548
- value.attach(scenarioOutline);
549
- return scenarioOutline;
550
- }
551
- return found;
552
- },
553
- findExample(name) {
554
- const found = value.closedScopes.find((child) => {
555
- if (!(child instanceof import_scopes.ScenarioScope)) {
556
- return false;
557
- }
558
- return child.name === name;
559
- });
560
- if (!found) {
561
- const scenario = new import_scopes.ScenarioScope(
562
- name,
563
- import_scopes2.Empty_Function,
564
- void 0,
565
- value.hooks,
566
- value.steps
567
- );
568
- value.attach(scenario);
569
- return scenario;
570
- }
571
- return found;
572
- },
573
- findStep: (keywordType, keyword, name) => {
574
- return value.steps.find(keywordType, keyword, name);
575
- }
576
- };
1106
+ // src/build-test-plan.ts
1107
+ function buildTestPlan(options) {
1108
+ const { feature, adapter } = options;
1109
+ assertFeature(feature);
1110
+ assertAdapter(adapter);
1111
+ const featureScope = options.featureScope ?? resolveFeatureScope(adapter, feature);
1112
+ return new TestPlanBuilder(feature, featureScope, adapter).build();
577
1113
  }
578
-
579
- // src/test-builder.ts
580
- var import_bind_decorator = require("@autometa/bind-decorator");
581
- var import_errors4 = require("@autometa/errors");
582
- var TestBuilder = class {
583
- constructor(feature) {
584
- this.feature = feature;
1114
+ function assertFeature(feature) {
1115
+ if (!feature) {
1116
+ throw new Error("A Gherkin feature is required to build a test plan");
585
1117
  }
586
- onFeatureExecuted(featureScope) {
587
- const bridge = new FeatureBridge();
588
- GherkinWalker.walk(
589
- {
590
- onFeature: (feature, accumulator) => {
591
- accumulator.data = { gherkin: feature, scope: featureScope };
592
- return accumulator;
593
- },
594
- onRule: (rule, accumulator) => {
595
- const ruleScope = scope(featureScope).findRule(
596
- rule.name
597
- );
598
- const bridge2 = new RuleBridge();
599
- bridge2.data = { gherkin: rule, scope: ruleScope };
600
- accumulator.rules.push(bridge2);
601
- return bridge2;
602
- },
603
- onScenario: (gherkin, accumulator) => {
604
- const scenarioScope = scope(accumulator.data.scope).findScenario(
605
- gherkin.name
606
- );
607
- const bridge2 = new ScenarioBridge();
608
- bridge2.data = { gherkin, scope: scenarioScope };
609
- accumulator.scenarios.push(bridge2);
610
- return bridge2;
611
- },
612
- onScenarioOutline: (gherkin, accumulator) => {
613
- const outlineScope = scope(
614
- accumulator.data.scope
615
- ).findScenarioOutline(gherkin.name);
616
- const bridge2 = new ScenarioOutlineBridge();
617
- bridge2.data = { gherkin, scope: outlineScope };
618
- accumulator.scenarios.push(bridge2);
619
- return bridge2;
620
- },
621
- onExamples: (gherkin, accumulator) => {
622
- const outlineScope = accumulator.data.scope;
623
- const bridge2 = new ExamplesBridge();
624
- bridge2.data = { gherkin, scope: outlineScope };
625
- accumulator.examples.push(bridge2);
626
- return bridge2;
627
- },
628
- onExample(gherkin, accumulator) {
629
- if (gherkin.table === void 0) {
630
- (0, import_errors4.raise)(
631
- `Example ${gherkin.name} has no Example Table data. A Row of data is required.`
632
- );
633
- }
634
- const title = gherkin.name;
635
- const exampleScope = scope(accumulator.data.scope).findExample(title);
636
- const bridge2 = new ExampleBridge();
637
- bridge2.data = { gherkin, scope: exampleScope };
638
- const acc = accumulator;
639
- acc.scenarios.push(bridge2);
640
- return bridge2;
641
- },
642
- onBackground(gherkin, accumulator) {
643
- const backgroundScope = scope(accumulator.data.scope).findBackground({
644
- name: gherkin.name
645
- });
646
- const bridge2 = new BackgroundBridge();
647
- bridge2.data = { gherkin, scope: backgroundScope };
648
- const acc = accumulator;
649
- acc.background = bridge2;
650
- return bridge2;
651
- },
652
- onStep: (step, accumulator) => {
653
- const {
654
- data: { scope: parentScope, gherkin }
655
- } = accumulator;
656
- const { keyword, keywordType, text } = step;
657
- const cache = parentScope.steps;
658
- const existing = getStep(
659
- accumulator,
660
- gherkin,
661
- cache,
662
- keywordType,
663
- keyword,
664
- text
665
- );
666
- const bridge2 = new StepBridge();
667
- const acc = accumulator;
668
- if (existing) {
669
- bridge2.data = {
670
- gherkin: step,
671
- scope: existing.step,
672
- args: existing.args
673
- };
674
- } else {
675
- (0, import_errors4.raise)(`No step definition matching ${step.keyword} ${step.text}`);
676
- }
677
- acc.steps.push(bridge2);
678
- return accumulator;
679
- }
680
- },
681
- this.feature,
682
- bridge
683
- );
684
- return bridge;
1118
+ }
1119
+ function assertAdapter(adapter) {
1120
+ if (!adapter) {
1121
+ throw new Error("A scope execution adapter is required to build a test plan");
685
1122
  }
686
- };
687
- __decorateClass([
688
- import_bind_decorator.Bind
689
- ], TestBuilder.prototype, "onFeatureExecuted", 1);
690
- function getStep(accumulator, gherkin, cache, keywordType, keyword, text) {
691
- try {
692
- if (accumulator instanceof ExampleBridge) {
693
- const scenario = gherkin;
694
- if (scenario.table) {
695
- return cache.findByExample(keywordType, keyword, text, scenario.table);
696
- }
697
- }
698
- return cache.find(keywordType, keyword, text);
699
- } catch (e) {
700
- const cause = e;
701
- const { title } = gherkin;
702
- (0, import_errors4.raise)(`'${title}' could not find a step definition`, { cause });
703
- }
704
- }
705
- // Annotate the CommonJS export names for ESM import in node:
706
- 0 && (module.exports = {
707
- BackgroundBridge,
708
- ExampleBridge,
709
- ExamplesBridge,
710
- FeatureBridge,
711
- GherkinCodeBridge,
712
- GherkinWalker,
713
- GlobalBridge,
714
- Query,
715
- RuleBridge,
716
- ScenarioBridge,
717
- ScenarioOutlineBridge,
718
- StepBridge,
719
- TestBuilder,
720
- find,
721
- findExamplesOrChild,
722
- findRuleOrChild,
723
- findRuleTypes,
724
- findScenario,
725
- findScenarioOutlineOrChild,
726
- findTestTypes,
727
- scope
728
- });
1123
+ }
1124
+
1125
+ export { buildTestPlan };
1126
+ //# sourceMappingURL=out.js.map
729
1127
  //# sourceMappingURL=index.js.map