@sun-asterisk/sungen 3.2.12-beta.1 → 3.2.12-beta.2

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.
@@ -25,7 +25,7 @@ import {
25
25
  import { ApiCatalogEntry, EnvironmentInfo, PlaywrightResult, ScreenSummary, TestCaseRow } from './types';
26
26
  import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
27
27
  import { SelectorKeyMap, substituteSelectorKeysInStep } from './selector-key-resolver';
28
- import { resolveResultVariants } from './result-variants';
28
+ import { formatVariantRow, resolveResultVariants } from './result-variants';
29
29
 
30
30
  export interface BuildCsvInput {
31
31
  screen: string;
@@ -70,7 +70,7 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
70
70
  // Data-driven (@cases): expand to one CSV row per executed dataset input (Playwright titles
71
71
  // "<scenario> — <label>"); plain scenarios resolve their single result. Shared with the
72
72
  // dashboard/JSON exporter via resolveResultVariants so the two can't drift on @cases handling.
73
- const variants = resolveResultVariants(m, input.results);
73
+ const variants = resolveResultVariants(m, input.results, input.testData);
74
74
 
75
75
  for (const variant of variants) {
76
76
  const displayName = `${m.feature.name}${variant.nameSuffix}`;
@@ -132,7 +132,11 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
132
132
  // Multi-line bullet format keeps delivery CSV/XLSX in sync with the
133
133
  // dashboard's Test data section. Multi-line CSV cells are valid (quoted
134
134
  // automatically) and render natively as multi-line in XLSX.
135
- const testData = formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n');
135
+ let testData = formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n');
136
+ // @cases: each report line shows ITS OWN dataset row (executed auto rows and manual
137
+ // split rows both carry variant.row) — one line per case, one dataset per line.
138
+ const variantRow = formatVariantRow(variant, '\n');
139
+ if (variantRow) testData = testData ? `${testData}\n${variantRow}` : variantRow;
136
140
 
137
141
  // Status for this (possibly per-input) row — resolved into `variant` above.
138
142
  const result: PlaywrightResult | undefined = variant.result;
@@ -9,7 +9,7 @@ import { FeatureMetadata, OrderedStep, ScenarioMetadata } from './types';
9
9
  /**
10
10
  * Variables referenced in a scenario: find all {{var_name}} in step text.
11
11
  */
12
- function extractReferencedVars(scenario: ParsedScenario): string[] {
12
+ function extractReferencedVars(scenario: ParsedScenario, includeComments = false): string[] {
13
13
  const vars = new Set<string>();
14
14
  for (const step of scenario.steps) {
15
15
  const matches = step.text.matchAll(/\{\{([a-zA-Z_][a-zA-Z0-9_.-]*)\}\}/g);
@@ -28,6 +28,19 @@ function extractReferencedVars(scenario: ParsedScenario): string[] {
28
28
  }
29
29
  }
30
30
  }
31
+ // Manual scenarios carry their real tester procedure in `# Tester verifies:` comment
32
+ // blocks — the {{var}} refs there are the data the tester needs, so include them
33
+ // (issue: manual test cases exported with an empty Test Data cell). Automated
34
+ // scenarios keep step-only scanning: their comments are notes (SPEC-GAP etc.) and
35
+ // would only add noise vars to the delivery report.
36
+ if (includeComments) {
37
+ for (const line of scenario.comments ?? []) {
38
+ const matches = line.matchAll(/\{\{([a-zA-Z_][a-zA-Z0-9_.-]*)\}\}/g);
39
+ for (const match of matches) {
40
+ vars.add(match[1]);
41
+ }
42
+ }
43
+ }
31
44
  return Array.from(vars);
32
45
  }
33
46
 
@@ -85,7 +98,7 @@ export function parseFeatureMetadata(featureFilePath: string): FeatureMetadata {
85
98
  tags: [...parsed.tags, ...sc.tags],
86
99
  stepsName: sc.stepsName,
87
100
  extendsName: sc.extendsName,
88
- referencedVars: extractReferencedVars(sc),
101
+ referencedVars: extractReferencedVars(sc, extractTestcaseType([...parsed.tags, ...sc.tags]) === 'Manual'),
89
102
  rawGivenSteps: given,
90
103
  rawWhenSteps: when,
91
104
  rawThenSteps: then,
@@ -23,7 +23,7 @@ import {
23
23
  } from './playwright-report-parser';
24
24
  import { ApiCatalogEntry, EnvironmentInfo, PlaywrightResult } from './types';
25
25
  import { SelectorKeyMap, substituteSelectorKeysInStep } from './selector-key-resolver';
26
- import { resolveResultVariants } from './result-variants';
26
+ import { formatVariantRow, resolveResultVariants } from './result-variants';
27
27
  import {
28
28
  ScenarioSnapshot,
29
29
  ScreenSnapshot,
@@ -123,13 +123,17 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
123
123
  }
124
124
  // Dashboard modal uses whitespace-pre-wrap — pass '\n' to render one
125
125
  // key/value per line instead of the CSV-friendly "; " join.
126
- const testDataStr = formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n');
126
+ const baseTestDataStr = formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n');
127
127
 
128
- // @cases scenarios expand to one snapshot entry per executed dataset row (shared with the CSV
129
- // exporter via resolveResultVariants), so their per-row pass/fail is counted not collapsed
130
- // into one Pending/N/A entry as before.
131
- const variants = resolveResultVariants(m, input.results ?? null);
128
+ // @cases scenarios expand to one snapshot entry per executed dataset row and manual
129
+ // @cases split per DATASET row (shared with the CSV exporter via resolveResultVariants),
130
+ // so their per-row pass/fail is counted — not collapsed into one Pending/N/A entry.
131
+ const variants = resolveResultVariants(m, input.results ?? null, input.testData);
132
132
  for (const variant of variants) {
133
+ // Per-line Test Data: shared vars + THIS variant's dataset row (parity with csv-exporter).
134
+ let testDataStr = baseTestDataStr;
135
+ const variantRow = formatVariantRow(variant, '\n');
136
+ if (variantRow) testDataStr = testDataStr ? `${testDataStr}\n${variantRow}` : variantRow;
133
137
  const displayName = `${m.feature.name}${variant.nameSuffix}`;
134
138
  const { vpId, category1 } = splitVpAndName(displayName);
135
139
  const tcId = generateTcId(input.screen, vpId, fallbackIndex);
@@ -5,12 +5,42 @@
5
5
  */
6
6
  import { MergedScenario } from './scenario-merger';
7
7
  import { PlaywrightResult } from './types';
8
+ import { extractTestcaseType } from './feature-parser';
8
9
 
9
10
  export interface ResultVariant {
10
- /** Appended to the scenario display name: `''` for a plain scenario, ` — <label>` per executed
11
+ /** Appended to the scenario display name: `''` for a plain scenario, ` — <label>` per
11
12
  * @cases dataset row (so each row gets a distinct category1). */
12
13
  nameSuffix: string;
13
14
  result?: PlaywrightResult;
15
+ /** The @cases dataset row backing this variant — lets the exporters render the row's own
16
+ * columns into the Test Data cell (`__`-prefixed bookkeeping keys excluded). Present on
17
+ * executed auto rows (matched by label) and on manual split rows. */
18
+ row?: Record<string, unknown>;
19
+ }
20
+
21
+ /**
22
+ * Read the `@cases:<name>` dataset rows for a scenario from the (flattened) test-data map.
23
+ * `loadTestData()` flattens arrays to JSON strings — accept that shape and raw arrays.
24
+ * Scalar rows normalize to `{ value }`, mirroring the runtime TestDataLoader.cases().
25
+ */
26
+ export function getCasesDatasetRows(
27
+ m: MergedScenario,
28
+ testData: Record<string, string> | null | undefined,
29
+ ): Array<Record<string, unknown>> | null {
30
+ const tag = m.feature.tags.find((t) => t.startsWith('@cases:'));
31
+ if (!tag || !testData) return null;
32
+ let list: unknown = testData[tag.slice('@cases:'.length)];
33
+ if (typeof list === 'string') {
34
+ try { list = JSON.parse(list); } catch { return null; }
35
+ }
36
+ if (!Array.isArray(list) || list.length === 0) return null;
37
+ return list.map((row) =>
38
+ row && typeof row === 'object' && !Array.isArray(row) ? (row as Record<string, unknown>) : { value: row });
39
+ }
40
+
41
+ /** Display label of a dataset row — mirrors the runtime TestDataLoader.cases() rule. */
42
+ function rowLabel(row: Record<string, unknown>, i: number): string {
43
+ return String(row.case ?? row.name ?? row.label ?? `row ${i + 1}`);
14
44
  }
15
45
 
16
46
  /**
@@ -18,9 +48,12 @@ export interface ResultVariant {
18
48
  *
19
49
  * A `@cases:<dataset>` scenario compiles to one `test()` per dataset row, whose Playwright title is
20
50
  * `<scenario> — <label>` — expand into one variant per EXECUTED row by matching that prefix (so an
21
- * already-run row carries its real pass/fail). A plain scenario resolves its single result by the
22
- * spec test title. With no results — or a @cases scenario with no matching result yet — a lone
23
- * variant with no result is returned, so the caller marks it Pending / N/A.
51
+ * already-run row carries its real pass/fail), attaching the backing dataset row by label.
52
+ *
53
+ * A MANUAL `@cases` scenario never executes, so that expansion can never fire — split it from the
54
+ * DATASET instead: one variant per row (` — <label>`, no result → Pending), each carrying its row
55
+ * so the manual tester gets one report line + its own data per case to walk and record. An auto
56
+ * @cases scenario that has not run yet stays a single line (its expansion belongs to execution).
24
57
  *
25
58
  * NOTE: this deliberately does NOT gate on `m.spec`. For @cases the compiled test title carries a
26
59
  * `— ${__row.__label}` suffix, so `findMatchingSpecTest` (base-name match) often leaves `m.spec`
@@ -30,13 +63,26 @@ export interface ResultVariant {
30
63
  export function resolveResultVariants(
31
64
  m: MergedScenario,
32
65
  results: Map<string, PlaywrightResult> | null,
66
+ testData?: Record<string, string> | null,
33
67
  ): ResultVariant[] {
34
68
  const isCases = m.feature.tags.some((t) => t.startsWith('@cases:'));
35
- if (isCases && results) {
36
- const marker = `${m.feature.name} — `;
37
- const rows = [...results.entries()].filter(([t]) => t.includes(marker));
38
- if (rows.length) {
39
- return rows.map(([t, r]) => ({ nameSuffix: ` — ${t.slice(t.indexOf(marker) + marker.length)}`, result: r }));
69
+ if (isCases) {
70
+ const datasetRows = getCasesDatasetRows(m, testData);
71
+ const byLabel = new Map<string, Record<string, unknown>>();
72
+ (datasetRows ?? []).forEach((row, i) => byLabel.set(rowLabel(row, i), row));
73
+
74
+ if (results) {
75
+ const marker = `${m.feature.name} — `;
76
+ const ran = [...results.entries()].filter(([t]) => t.includes(marker));
77
+ if (ran.length) {
78
+ return ran.map(([t, r]) => {
79
+ const label = t.slice(t.indexOf(marker) + marker.length);
80
+ return { nameSuffix: ` — ${label}`, result: r, row: byLabel.get(label) };
81
+ });
82
+ }
83
+ }
84
+ if (datasetRows && extractTestcaseType(m.feature.tags) === 'Manual') {
85
+ return datasetRows.map((row, i) => ({ nameSuffix: ` — ${rowLabel(row, i)}`, row }));
40
86
  }
41
87
  return [{ nameSuffix: '' }];
42
88
  }
@@ -45,3 +91,16 @@ export function resolveResultVariants(
45
91
  }
46
92
  return [{ nameSuffix: '' }];
47
93
  }
94
+
95
+ /**
96
+ * Render a variant's dataset row for the Test Data cell: one `col: value` line per column,
97
+ * `__`-prefixed bookkeeping keys excluded. Returns '' when the variant carries no row.
98
+ */
99
+ export function formatVariantRow(variant: ResultVariant, separator: string = '\n'): string {
100
+ if (!variant.row) return '';
101
+ const useBullet = separator.includes('\n');
102
+ return Object.entries(variant.row)
103
+ .filter(([k]) => !k.startsWith('__'))
104
+ .map(([k, v]) => `${useBullet ? '• ' : ''}${k}: ${v}`)
105
+ .join(separator);
106
+ }
@@ -133,6 +133,7 @@ function truncate(s: string, max: number): string {
133
133
  return s.substring(0, max - 1) + '…';
134
134
  }
135
135
 
136
+
136
137
  /**
137
138
  * Resolve `{{var}}` test-data placeholders in a step / expected-result line to their concrete values,
138
139
  * so the delivery report shows what QA actually verifies (e.g. `text contains {{app_version}}` →