executable-stories-formatters 0.15.1 → 0.16.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/cli.js CHANGED
@@ -702,6 +702,41 @@ function generateFeatureId(uri) {
702
702
  function generateScenarioId(featureId, scenarioName) {
703
703
  return `${featureId};${slugify(scenarioName)}`;
704
704
  }
705
+ function normalizeText(text2) {
706
+ return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
707
+ }
708
+ function tokenize(text2) {
709
+ const normalized = normalizeText(text2);
710
+ return normalized.length === 0 ? [] : normalized.split(" ");
711
+ }
712
+ function behaviourFingerprint(input) {
713
+ const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
714
+ const covers = [...input.covers ?? []].map((path18) => path18.trim()).filter(Boolean).sort().join(",");
715
+ if (steps.length === 0 && covers.length === 0) return "";
716
+ return createHash("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
717
+ }
718
+ function jaccard(a, b) {
719
+ if (a.size === 0 && b.size === 0) return 1;
720
+ if (a.size === 0 || b.size === 0) return 0;
721
+ let intersection = 0;
722
+ for (const value of a) if (b.has(value)) intersection += 1;
723
+ return intersection / (a.size + b.size - intersection);
724
+ }
725
+ function stepTokens(steps) {
726
+ const tokens = /* @__PURE__ */ new Set();
727
+ for (const step of steps) {
728
+ for (const token of tokenize(step.text)) tokens.add(token);
729
+ }
730
+ return tokens;
731
+ }
732
+ function behaviourSimilarity(a, b) {
733
+ const aSteps = stepTokens(a.steps);
734
+ const bSteps = stepTokens(b.steps);
735
+ if (aSteps.size === 0 || bSteps.size === 0) return 0;
736
+ const stepScore = jaccard(aSteps, bSteps);
737
+ const titleScore = jaccard(new Set(tokenize(a.scenario)), new Set(tokenize(b.scenario)));
738
+ return stepScore * 0.7 + titleScore * 0.3;
739
+ }
705
740
 
706
741
  // src/converters/acl/steps.ts
707
742
  function deriveStepResults(steps, scenarioStatus, error) {
@@ -17401,6 +17436,10 @@ function titleFor(kind) {
17401
17436
  return "Added";
17402
17437
  case "removed":
17403
17438
  return "Removed";
17439
+ case "renamed":
17440
+ return "Renamed";
17441
+ case "moved":
17442
+ return "Moved";
17404
17443
  case "changed":
17405
17444
  return "Changed";
17406
17445
  default:
@@ -17449,6 +17488,8 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
17449
17488
  addSection(lines, diff, "fixed", maxScenarios);
17450
17489
  addSection(lines, diff, "added", maxScenarios);
17451
17490
  addSection(lines, diff, "removed", maxScenarios);
17491
+ addSection(lines, diff, "renamed", maxScenarios);
17492
+ addSection(lines, diff, "moved", maxScenarios);
17452
17493
  addSection(lines, diff, "changed", maxScenarios);
17453
17494
  return lines.join("\n").trimEnd();
17454
17495
  }
@@ -17525,8 +17566,10 @@ function sortDiffs(scenarios) {
17525
17566
  fixed: 1,
17526
17567
  added: 2,
17527
17568
  removed: 3,
17528
- changed: 4,
17529
- unchanged: 5
17569
+ renamed: 4,
17570
+ moved: 5,
17571
+ changed: 6,
17572
+ unchanged: 7
17530
17573
  };
17531
17574
  return [...scenarios].sort((a, b) => {
17532
17575
  if (rank[a.kind] !== rank[b.kind]) {
@@ -17541,69 +17584,108 @@ function sortDiffs(scenarios) {
17541
17584
  return a.scenario.localeCompare(b.scenario);
17542
17585
  });
17543
17586
  }
17587
+ var SIMILARITY_THRESHOLD = 0.75;
17588
+ function identityInput(tc) {
17589
+ return {
17590
+ scenario: tc.story.scenario,
17591
+ sourceFile: tc.sourceFile,
17592
+ steps: tc.story.steps,
17593
+ covers: tc.story.covers
17594
+ };
17595
+ }
17596
+ function changedFieldsOf(flags) {
17597
+ return Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
17598
+ }
17599
+ function allChangedFlags(errorMessage) {
17600
+ return {
17601
+ status: true,
17602
+ steps: true,
17603
+ docs: true,
17604
+ tags: true,
17605
+ tickets: true,
17606
+ source: true,
17607
+ duration: true,
17608
+ attachments: true,
17609
+ error: Boolean(errorMessage),
17610
+ titlePath: true
17611
+ };
17612
+ }
17613
+ function matchIdentities(removed, added) {
17614
+ const pairs = [];
17615
+ const remainingRemoved = new Set(removed);
17616
+ const remainingAdded = new Set(added);
17617
+ const removedByFp = /* @__PURE__ */ new Map();
17618
+ const addedByFp = /* @__PURE__ */ new Map();
17619
+ for (const tc of remainingRemoved) {
17620
+ const fp = behaviourFingerprint(identityInput(tc));
17621
+ if (fp === "") continue;
17622
+ (removedByFp.get(fp) ?? removedByFp.set(fp, []).get(fp)).push(tc);
17623
+ }
17624
+ for (const tc of remainingAdded) {
17625
+ const fp = behaviourFingerprint(identityInput(tc));
17626
+ if (fp === "") continue;
17627
+ (addedByFp.get(fp) ?? addedByFp.set(fp, []).get(fp)).push(tc);
17628
+ }
17629
+ for (const [fp, removedGroup] of removedByFp) {
17630
+ const addedGroup = addedByFp.get(fp);
17631
+ if (removedGroup.length === 1 && addedGroup && addedGroup.length === 1) {
17632
+ pairs.push({ before: removedGroup[0], after: addedGroup[0], confidence: 1, matchedBy: "fingerprint" });
17633
+ remainingRemoved.delete(removedGroup[0]);
17634
+ remainingAdded.delete(addedGroup[0]);
17635
+ }
17636
+ }
17637
+ for (const before of [...remainingRemoved]) {
17638
+ let best;
17639
+ let bestScore = 0;
17640
+ let tied = false;
17641
+ for (const after of remainingAdded) {
17642
+ const score = behaviourSimilarity(identityInput(before), identityInput(after));
17643
+ if (score > bestScore) {
17644
+ bestScore = score;
17645
+ best = after;
17646
+ tied = false;
17647
+ } else if (score === bestScore) {
17648
+ tied = true;
17649
+ }
17650
+ }
17651
+ if (best && !tied && bestScore >= SIMILARITY_THRESHOLD) {
17652
+ pairs.push({ before, after: best, confidence: Math.round(bestScore * 100) / 100, matchedBy: "similarity" });
17653
+ remainingRemoved.delete(before);
17654
+ remainingAdded.delete(best);
17655
+ }
17656
+ }
17657
+ return {
17658
+ pairs,
17659
+ unmatchedRemoved: [...remainingRemoved],
17660
+ unmatchedAdded: [...remainingAdded]
17661
+ };
17662
+ }
17663
+ function identityKind(before, after) {
17664
+ const titleChanged = before.story.scenario !== after.story.scenario;
17665
+ const fileChanged = before.sourceFile !== after.sourceFile;
17666
+ return fileChanged && !titleChanged ? "moved" : "renamed";
17667
+ }
17544
17668
  function diffRuns(baseline, current) {
17545
17669
  const baselineById = new Map(baseline.testCases.map((tc) => [tc.id, tc]));
17546
17670
  const currentById = new Map(current.testCases.map((tc) => [tc.id, tc]));
17547
17671
  const ids = /* @__PURE__ */ new Set([...baselineById.keys(), ...currentById.keys()]);
17548
17672
  const scenarios = [];
17673
+ const removedCases = [];
17674
+ const addedCases = [];
17549
17675
  for (const id of ids) {
17550
17676
  const before = baselineById.get(id);
17551
17677
  const after = currentById.get(id);
17552
17678
  if (!before && after) {
17553
- const flags2 = {
17554
- status: true,
17555
- steps: true,
17556
- docs: true,
17557
- tags: true,
17558
- tickets: true,
17559
- source: true,
17560
- duration: true,
17561
- attachments: true,
17562
- error: Boolean(after.errorMessage),
17563
- titlePath: true
17564
- };
17565
- scenarios.push({
17566
- kind: "added",
17567
- id,
17568
- scenario: after.story.scenario,
17569
- sourceFile: after.sourceFile,
17570
- sourceLine: after.sourceLine,
17571
- current: toScenarioSnapshot(after),
17572
- flags: flags2,
17573
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
17574
- });
17679
+ addedCases.push(after);
17575
17680
  continue;
17576
17681
  }
17577
17682
  if (before && !after) {
17578
- const flags2 = {
17579
- status: true,
17580
- steps: true,
17581
- docs: true,
17582
- tags: true,
17583
- tickets: true,
17584
- source: true,
17585
- duration: true,
17586
- attachments: true,
17587
- error: Boolean(before.errorMessage),
17588
- titlePath: true
17589
- };
17590
- scenarios.push({
17591
- kind: "removed",
17592
- id,
17593
- scenario: before.story.scenario,
17594
- sourceFile: before.sourceFile,
17595
- sourceLine: before.sourceLine,
17596
- baseline: toScenarioSnapshot(before),
17597
- flags: flags2,
17598
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
17599
- });
17600
- continue;
17601
- }
17602
- if (!before || !after) {
17683
+ removedCases.push(before);
17603
17684
  continue;
17604
17685
  }
17686
+ if (!before || !after) continue;
17605
17687
  const flags = buildFlags(before, after);
17606
- const changedFields = Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
17688
+ const changedFields = changedFieldsOf(flags);
17607
17689
  const kind = getPrimaryKind(before, after, changedFields.length > 0);
17608
17690
  scenarios.push({
17609
17691
  kind,
@@ -17618,12 +17700,59 @@ function diffRuns(baseline, current) {
17618
17700
  durationDeltaMs: after.durationMs - before.durationMs
17619
17701
  });
17620
17702
  }
17703
+ const { pairs, unmatchedRemoved, unmatchedAdded } = matchIdentities(removedCases, addedCases);
17704
+ for (const { before, after, confidence, matchedBy } of pairs) {
17705
+ const flags = buildFlags(before, after);
17706
+ scenarios.push({
17707
+ kind: identityKind(before, after),
17708
+ id: after.id,
17709
+ previousId: before.id,
17710
+ scenario: after.story.scenario,
17711
+ sourceFile: after.sourceFile,
17712
+ sourceLine: after.sourceLine,
17713
+ baseline: toScenarioSnapshot(before),
17714
+ current: toScenarioSnapshot(after),
17715
+ flags,
17716
+ changedFields: changedFieldsOf(flags),
17717
+ durationDeltaMs: after.durationMs - before.durationMs,
17718
+ matchConfidence: confidence,
17719
+ matchedBy
17720
+ });
17721
+ }
17722
+ for (const after of unmatchedAdded) {
17723
+ const flags = allChangedFlags(after.errorMessage);
17724
+ scenarios.push({
17725
+ kind: "added",
17726
+ id: after.id,
17727
+ scenario: after.story.scenario,
17728
+ sourceFile: after.sourceFile,
17729
+ sourceLine: after.sourceLine,
17730
+ current: toScenarioSnapshot(after),
17731
+ flags,
17732
+ changedFields: changedFieldsOf(flags)
17733
+ });
17734
+ }
17735
+ for (const before of unmatchedRemoved) {
17736
+ const flags = allChangedFlags(before.errorMessage);
17737
+ scenarios.push({
17738
+ kind: "removed",
17739
+ id: before.id,
17740
+ scenario: before.story.scenario,
17741
+ sourceFile: before.sourceFile,
17742
+ sourceLine: before.sourceLine,
17743
+ baseline: toScenarioSnapshot(before),
17744
+ flags,
17745
+ changedFields: changedFieldsOf(flags)
17746
+ });
17747
+ }
17621
17748
  const sorted = sortDiffs(scenarios);
17622
17749
  const summary = {
17623
17750
  totalBaseline: baseline.testCases.length,
17624
17751
  totalCurrent: current.testCases.length,
17625
17752
  added: sorted.filter((s) => s.kind === "added").length,
17626
17753
  removed: sorted.filter((s) => s.kind === "removed").length,
17754
+ renamed: sorted.filter((s) => s.kind === "renamed").length,
17755
+ moved: sorted.filter((s) => s.kind === "moved").length,
17627
17756
  changed: sorted.filter((s) => s.kind === "changed").length,
17628
17757
  regressed: sorted.filter((s) => s.kind === "regressed").length,
17629
17758
  fixed: sorted.filter((s) => s.kind === "fixed").length,
@@ -17651,6 +17780,10 @@ function statusLabel(kind) {
17651
17780
  return "Added";
17652
17781
  case "removed":
17653
17782
  return "Removed";
17783
+ case "renamed":
17784
+ return "Renamed";
17785
+ case "moved":
17786
+ return "Moved";
17654
17787
  case "changed":
17655
17788
  return "Changed";
17656
17789
  default:
@@ -17976,6 +18109,8 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', fun
17976
18109
  <div class="summary-card"><strong>${diff.summary.fixed}</strong><span>Fixed</span></div>
17977
18110
  <div class="summary-card"><strong>${diff.summary.added}</strong><span>Added</span></div>
17978
18111
  <div class="summary-card"><strong>${diff.summary.removed}</strong><span>Removed</span></div>
18112
+ <div class="summary-card"><strong>${diff.summary.renamed}</strong><span>Renamed</span></div>
18113
+ <div class="summary-card"><strong>${diff.summary.moved}</strong><span>Moved</span></div>
17979
18114
  <div class="summary-card"><strong>${diff.summary.changed}</strong><span>Changed</span></div>
17980
18115
  <div class="summary-card"><strong>${diff.summary.unchanged}</strong><span>Unchanged</span></div>
17981
18116
  </section>
@@ -18036,6 +18171,10 @@ function formatStatus(kind) {
18036
18171
  return "Added";
18037
18172
  case "removed":
18038
18173
  return "Removed";
18174
+ case "renamed":
18175
+ return "Renamed";
18176
+ case "moved":
18177
+ return "Moved";
18039
18178
  case "changed":
18040
18179
  return "Changed";
18041
18180
  default:
@@ -18188,13 +18327,13 @@ var RunDiffMarkdownFormatter = class {
18188
18327
  lines.push("No regressions or fixes detected. Remaining changes are neutral.");
18189
18328
  }
18190
18329
  lines.push("");
18191
- lines.push("| Added | Removed | Regressed | Fixed | Changed | Unchanged |");
18192
- lines.push("| ---: | ---: | ---: | ---: | ---: | ---: |");
18330
+ lines.push("| Added | Removed | Renamed | Moved | Regressed | Fixed | Changed | Unchanged |");
18331
+ lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
18193
18332
  lines.push(
18194
- `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
18333
+ `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.renamed} | ${diff.summary.moved} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
18195
18334
  );
18196
18335
  lines.push("");
18197
- for (const kind of ["regressed", "fixed", "added", "removed", "changed"]) {
18336
+ for (const kind of ["regressed", "fixed", "added", "removed", "renamed", "moved", "changed"]) {
18198
18337
  const scenarios = diff.scenarios.filter((scenario) => scenario.kind === kind);
18199
18338
  if (scenarios.length === 0) continue;
18200
18339
  lines.push(`## ${formatStatus(kind)} (${scenarios.length})`);
@@ -19143,7 +19282,10 @@ function diffStoryReports(baseline, current) {
19143
19282
  };
19144
19283
  });
19145
19284
  const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
19146
- for (const s of scenarios) summary[s.kind] += 1;
19285
+ for (const s of scenarios) {
19286
+ if (s.kind === "renamed" || s.kind === "moved") continue;
19287
+ summary[s.kind] += 1;
19288
+ }
19147
19289
  return { schemaVersion: "1.0", summary, scenarios };
19148
19290
  }
19149
19291