@autometa/test-builder 0.4.2 → 1.0.0-rc.1

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