@sun-asterisk/sungen 3.2.5 → 3.2.7
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/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +7 -46
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +28 -6
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +20 -20
- package/dist/dashboard/types.d.ts +18 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/api-catalog-loader.d.ts +18 -0
- package/dist/exporters/api-catalog-loader.d.ts.map +1 -0
- package/dist/exporters/api-catalog-loader.js +77 -0
- package/dist/exporters/api-catalog-loader.js.map +1 -0
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +20 -26
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +6 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +9 -3
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts +10 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +92 -50
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +14 -0
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +30 -0
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/result-variants.d.ts +29 -0
- package/dist/exporters/result-variants.d.ts.map +1 -0
- package/dist/exporters/result-variants.js +33 -0
- package/dist/exporters/result-variants.js.map +1 -0
- package/dist/exporters/xlsx-exporter.d.ts +18 -17
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +25 -563
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts +196 -0
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -0
- package/dist/exporters/xlsx-report-builder.js +653 -0
- package/dist/exporters/xlsx-report-builder.js.map +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/scenario.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +6 -6
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +30 -8
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +16 -1
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.js +23 -0
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +7 -0
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +13 -2
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +9 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +66 -9
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-api.md +76 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
- package/dist/orchestrator/templates/ai-src/config/codex.md +3 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +73 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +42 -8
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +21 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +2 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +33 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +6 -0
- package/dist/orchestrator/templates/specs-api.d.ts +26 -0
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +131 -3
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +118 -5
- package/dist/orchestrator/templates/specs-test-data.ts +20 -1
- package/package.json +3 -3
- package/src/cli/commands/delivery.ts +8 -42
- package/src/dashboard/snapshot-builder.ts +31 -7
- package/src/dashboard/templates/index.html +20 -20
- package/src/dashboard/types.ts +19 -0
- package/src/exporters/api-catalog-loader.ts +44 -0
- package/src/exporters/csv-exporter.ts +19 -24
- package/src/exporters/feature-parser.ts +8 -2
- package/src/exporters/json-exporter.ts +102 -51
- package/src/exporters/playwright-report-parser.ts +27 -0
- package/src/exporters/result-variants.ts +47 -0
- package/src/exporters/xlsx-exporter.ts +34 -623
- package/src/exporters/xlsx-report-builder.ts +806 -0
- package/src/generators/test-generator/adapters/appium/templates/scenario.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +6 -6
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +30 -8
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +16 -1
- package/src/generators/test-generator/patterns/expect-patterns.ts +23 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +30 -2
- package/src/orchestrator/project-initializer.ts +67 -9
- package/src/orchestrator/templates/ai-src/commands/add-api.md +76 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
- package/src/orchestrator/templates/ai-src/config/codex.md +3 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +73 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +42 -8
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +21 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +2 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +33 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +6 -0
- package/src/orchestrator/templates/specs-api.ts +118 -5
- package/src/orchestrator/templates/specs-test-data.ts +20 -1
- package/dist/cli/types.d.ts +0 -9
- package/dist/cli/types.d.ts.map +0 -1
- package/dist/cli/types.js +0 -7
- package/dist/cli/types.js.map +0 -1
- package/dist/generators/gherkin-parser/selector-extractor.d.ts +0 -37
- package/dist/generators/gherkin-parser/selector-extractor.d.ts.map +0 -1
- package/dist/generators/gherkin-parser/selector-extractor.js +0 -108
- package/dist/generators/gherkin-parser/selector-extractor.js.map +0 -1
- package/dist/generators/test-generator/types.d.ts +0 -25
- package/dist/generators/test-generator/types.d.ts.map +0 -1
- package/dist/generators/test-generator/types.js +0 -7
- package/dist/generators/test-generator/types.js.map +0 -1
- package/dist/generators/types.d.ts +0 -119
- package/dist/generators/types.d.ts.map +0 -1
- package/dist/generators/types.js +0 -48
- package/dist/generators/types.js.map +0 -1
- package/dist/orchestrator/figma/node-path-collapser.d.ts +0 -16
- package/dist/orchestrator/figma/node-path-collapser.d.ts.map +0 -1
- package/dist/orchestrator/figma/node-path-collapser.js +0 -37
- package/dist/orchestrator/figma/node-path-collapser.js.map +0 -1
- package/src/cli/types.ts +0 -9
- package/src/generators/gherkin-parser/selector-extractor.ts +0 -142
- package/src/generators/test-generator/types.ts +0 -26
- package/src/generators/types.ts +0 -227
- package/src/orchestrator/figma/node-path-collapser.ts +0 -38
package/src/dashboard/types.ts
CHANGED
|
@@ -76,6 +76,25 @@ export interface FeatureSnapshot {
|
|
|
76
76
|
locales?: LocaleSnapshot[]; // locale overlays WITHIN this entry (base + at least one overlay)
|
|
77
77
|
platform?: 'android' | 'ios'; // one OS run of a @platform:mobile feature (separate entry per OS)
|
|
78
78
|
executedPlatforms?: ('android' | 'ios')[]; // OSes that produced a result file this run (mobile only)
|
|
79
|
+
/** API detail sheet rows (api-kind units only) — precomputed by the snapshot
|
|
80
|
+
* builder with the same buildApiDetailRows call the delivery CLI uses, so the
|
|
81
|
+
* dashboard XLSX export appends an "API detail" sheet identical to delivery's. */
|
|
82
|
+
apiDetail?: ApiDetailRow[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* One endpoint row of the "API detail" sheet (api-kind units). Structural copy
|
|
87
|
+
* of ApiDetailRow in src/exporters/types.ts — kept local so this file stays
|
|
88
|
+
* import-free (it is mirrored verbatim into dashboard/src/types.ts).
|
|
89
|
+
*/
|
|
90
|
+
export interface ApiDetailRow {
|
|
91
|
+
endpoint: string; // catalog path, e.g. /users/count?email=:email
|
|
92
|
+
method: string; // HTTP method
|
|
93
|
+
authDatasource: string; // datasource + any @auth tag
|
|
94
|
+
requestShape: string; // body fields / params / encoding
|
|
95
|
+
expectedStatusMatrix: string; // expect.status + @cases dataset pointer
|
|
96
|
+
flowSteps: string; // ordered @api:<name> chain for flow scenarios
|
|
97
|
+
concurrencyInvariant: string; // @concurrent invariant, when present
|
|
79
98
|
}
|
|
80
99
|
|
|
81
100
|
/**
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load the apis.yaml endpoint catalog for an api-kind unit — shared by the
|
|
3
|
+
* delivery CLI (CSV/XLSX export) and the dashboard snapshot builder so both
|
|
4
|
+
* surfaces render the identical request/response contract for API test cases.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { parse as parseYaml } from 'yaml';
|
|
10
|
+
import { ApiCatalogEntry } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load the apis.yaml catalog for an api-kind unit, merging the shared catalog under
|
|
14
|
+
* the unit catalog. The runtime resolves an endpoint unit-scope-first then falls back
|
|
15
|
+
* to the shared catalog (driver-api api-catalog.ts), so an endpoint defined only in
|
|
16
|
+
* `qa/api/apis.yaml` still runs — we must load it too, or its request block would come
|
|
17
|
+
* back empty (the very bug this feature fixes). Unit wins on a name collision.
|
|
18
|
+
* Returns an empty object when neither file is present (graceful degradation).
|
|
19
|
+
*/
|
|
20
|
+
export function loadApiCatalog(
|
|
21
|
+
cwd: string,
|
|
22
|
+
kind: 'screen' | 'flow' | 'api',
|
|
23
|
+
screen: string,
|
|
24
|
+
): Record<string, ApiCatalogEntry> {
|
|
25
|
+
if (kind !== 'api') return {};
|
|
26
|
+
const shared = readApiCatalogFile(path.join(cwd, 'qa', 'api', 'apis.yaml'));
|
|
27
|
+
const unit = readApiCatalogFile(path.join(cwd, 'qa', 'api', screen, 'api', 'apis.yaml'));
|
|
28
|
+
return { ...shared, ...unit };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Parse one apis.yaml file into an endpoint map, unwrapping the optional `apis:` wrapper. */
|
|
32
|
+
export function readApiCatalogFile(catalogPath: string): Record<string, ApiCatalogEntry> {
|
|
33
|
+
if (!fs.existsSync(catalogPath)) return {};
|
|
34
|
+
try {
|
|
35
|
+
const parsed = parseYaml(fs.readFileSync(catalogPath, 'utf-8')) as Record<string, unknown> | null;
|
|
36
|
+
if (parsed && typeof parsed === 'object') {
|
|
37
|
+
// The runtime loader accepts both a flat map and an `apis:` wrapper — unwrap it
|
|
38
|
+
// here too so both loaders read the same file identically.
|
|
39
|
+
const root = parsed.apis && typeof parsed.apis === 'object' ? parsed.apis : parsed;
|
|
40
|
+
return root as Record<string, ApiCatalogEntry>;
|
|
41
|
+
}
|
|
42
|
+
} catch { /* malformed yaml — skip */ }
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
@@ -25,6 +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
29
|
|
|
29
30
|
export interface BuildCsvInput {
|
|
30
31
|
screen: string;
|
|
@@ -66,21 +67,10 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
66
67
|
let fallbackIndex = 1;
|
|
67
68
|
|
|
68
69
|
for (const m of input.merged) {
|
|
69
|
-
// Data-driven (@cases):
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
let variants: Array<{ nameSuffix: string; result?: PlaywrightResult }> = [{ nameSuffix: '' }];
|
|
74
|
-
if (isCases && input.results) {
|
|
75
|
-
// Result titles are "<describe> > <scenario> — <label>"; match the scenario+label marker.
|
|
76
|
-
const marker = `${m.feature.name} — `;
|
|
77
|
-
const rowResults = [...input.results.entries()].filter(([t]) => t.includes(marker));
|
|
78
|
-
if (rowResults.length) {
|
|
79
|
-
variants = rowResults.map(([t, r]) => ({ nameSuffix: ` — ${t.slice(t.indexOf(marker) + marker.length)}`, result: r }));
|
|
80
|
-
}
|
|
81
|
-
} else if (input.results && m.spec) {
|
|
82
|
-
variants = [{ nameSuffix: '', result: input.results.get(m.spec.testTitle) }];
|
|
83
|
-
}
|
|
70
|
+
// Data-driven (@cases): expand to one CSV row per executed dataset input (Playwright titles
|
|
71
|
+
// "<scenario> — <label>"); plain scenarios resolve their single result. Shared with the
|
|
72
|
+
// dashboard/JSON exporter via resolveResultVariants so the two can't drift on @cases handling.
|
|
73
|
+
const variants = resolveResultVariants(m, input.results);
|
|
84
74
|
|
|
85
75
|
for (const variant of variants) {
|
|
86
76
|
const displayName = `${m.feature.name}${variant.nameSuffix}`;
|
|
@@ -154,21 +144,26 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
154
144
|
let environment = '';
|
|
155
145
|
let executor = '';
|
|
156
146
|
|
|
157
|
-
if (
|
|
158
|
-
//
|
|
147
|
+
if (result) {
|
|
148
|
+
// An execution record exists → the scenario compiled AND ran; that is authoritative and
|
|
149
|
+
// MUST win over the `!m.spec` "not compiled" heuristic below. A @cases scenario compiles to
|
|
150
|
+
// per-row test titles carrying a `— <label>` suffix, so `findMatchingSpecTest` (base-name
|
|
151
|
+
// match) frequently leaves `m.spec` null even though the rows ran — without checking `result`
|
|
152
|
+
// first, those already-passed/failed rows were mislabelled N/A ("not compiled").
|
|
153
|
+
testResult = statusToTestResult(result.status);
|
|
154
|
+
executedDate = formatExecutedDate(result.startTime);
|
|
155
|
+
note = formatNote(result, expectedResults);
|
|
156
|
+
environment = formatEnv(input.env.baseURL, input.featurePath, result.projectName || input.env.projectName);
|
|
157
|
+
executor = input.env.executor;
|
|
158
|
+
} else if (!m.spec) {
|
|
159
|
+
// No execution record AND no compiled test → Manual if @manual, else genuinely not compiled.
|
|
159
160
|
testResult = testcaseType === 'Manual' ? 'Pending' : 'N/A';
|
|
160
161
|
if (testcaseType !== 'Manual') {
|
|
161
162
|
note = 'Scenario not compiled — re-run `sungen generate --screen ' + input.screen + '`';
|
|
162
163
|
}
|
|
163
|
-
} else
|
|
164
|
+
} else {
|
|
164
165
|
// Compiled but no execution record → Pending
|
|
165
166
|
testResult = 'Pending';
|
|
166
|
-
} else {
|
|
167
|
-
testResult = statusToTestResult(result.status);
|
|
168
|
-
executedDate = formatExecutedDate(result.startTime);
|
|
169
|
-
note = formatNote(result, expectedResults);
|
|
170
|
-
environment = formatEnv(input.env.baseURL, input.featurePath, result.projectName || input.env.projectName);
|
|
171
|
-
executor = input.env.executor;
|
|
172
167
|
}
|
|
173
168
|
|
|
174
169
|
// If we have environment info and the test actually ran, populate env + executor
|
|
@@ -144,11 +144,17 @@ export function extractTestcaseType(tags: string[]): 'Auto' | 'Manual' {
|
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
146
|
* Extract auth role from tags. Returns 'no-auth' if @no-auth, else role name or null.
|
|
147
|
+
*
|
|
148
|
+
* Merged tag arrays put feature-level tags before scenario-level ones
|
|
149
|
+
* (`parseFeatureMetadata` builds `[...featureTags, ...scenarioTags]`), so the
|
|
150
|
+
* LAST @auth: tag is the most specific — a scenario-level @auth:owner must
|
|
151
|
+
* override a feature-level @auth:user in the delivery Pre-condition column,
|
|
152
|
+
* matching the codegen precedence (getEffectiveAuthRole: Scenario > Feature).
|
|
147
153
|
*/
|
|
148
154
|
export function extractAuthRole(tags: string[]): string | null {
|
|
149
155
|
if (tags.includes('@no-auth')) return 'no-auth';
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
156
|
+
const authTags = tags.filter((t) => t.startsWith('@auth:'));
|
|
157
|
+
if (authTags.length > 0) return authTags[authTags.length - 1].replace('@auth:', '');
|
|
152
158
|
return null;
|
|
153
159
|
}
|
|
154
160
|
|
|
@@ -15,13 +15,15 @@ import {
|
|
|
15
15
|
splitVpAndName,
|
|
16
16
|
} from './feature-parser';
|
|
17
17
|
import { formatPrecondition, keepActor, stripActor } from './step-formatter';
|
|
18
|
-
import { formatTestData } from './test-data-resolver';
|
|
18
|
+
import { formatTestData, substituteTestDataVars } from './test-data-resolver';
|
|
19
|
+
import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
|
|
19
20
|
import {
|
|
20
21
|
formatExecutedDate,
|
|
21
22
|
statusToTestResult,
|
|
22
23
|
} from './playwright-report-parser';
|
|
23
|
-
import { EnvironmentInfo, PlaywrightResult } from './types';
|
|
24
|
+
import { ApiCatalogEntry, EnvironmentInfo, PlaywrightResult } from './types';
|
|
24
25
|
import { SelectorKeyMap, substituteSelectorKeysInStep } from './selector-key-resolver';
|
|
26
|
+
import { resolveResultVariants } from './result-variants';
|
|
25
27
|
import {
|
|
26
28
|
ScenarioSnapshot,
|
|
27
29
|
ScreenSnapshot,
|
|
@@ -52,6 +54,15 @@ export interface BuildScreenSnapshotInput {
|
|
|
52
54
|
* link" instead of the VI key "Click [VỀ KUDOS] link".
|
|
53
55
|
*/
|
|
54
56
|
selectorKeyMap?: SelectorKeyMap;
|
|
57
|
+
/**
|
|
58
|
+
* Unit kind. When `'api'`, Steps/Expected are composed from the request
|
|
59
|
+
* contract (Method/API/Header/Body) and response contract (Status + Response)
|
|
60
|
+
* exactly like csv-exporter's buildTestCaseRows — so the dashboard UI and its
|
|
61
|
+
* XLSX export show the same cells as the delivery CSV.
|
|
62
|
+
*/
|
|
63
|
+
kind?: 'screen' | 'flow' | 'api';
|
|
64
|
+
/** apis.yaml catalog (keyed by endpoint name) for api-kind units. */
|
|
65
|
+
apiCatalog?: Record<string, ApiCatalogEntry>;
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
/**
|
|
@@ -62,11 +73,6 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
62
73
|
let fallbackIndex = 1;
|
|
63
74
|
|
|
64
75
|
for (const m of input.merged) {
|
|
65
|
-
const { vpId, category1 } = splitVpAndName(m.feature.name);
|
|
66
|
-
const tcId = generateTcId(input.screen, vpId, fallbackIndex);
|
|
67
|
-
if (!vpId) fallbackIndex++;
|
|
68
|
-
|
|
69
|
-
const category2 = mapVpToCategory2(vpId, category1);
|
|
70
76
|
const priority = extractPriority(m.feature.tags) as ScenarioSnapshot['priority'];
|
|
71
77
|
const baseType = extractTestcaseType(m.feature.tags);
|
|
72
78
|
const authRole = extractAuthRole(m.feature.tags);
|
|
@@ -81,33 +87,70 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
81
87
|
: text;
|
|
82
88
|
const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
83
89
|
localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
let steps = m.resolvedSteps.map(formatStep).filter(Boolean);
|
|
91
|
+
let expectedResults = m.resolvedExpected.map(formatStep).filter(Boolean);
|
|
92
|
+
|
|
93
|
+
// API test cases: mirror csv-exporter's request/response rendering so the
|
|
94
|
+
// dashboard (UI + XLSX export) shows the same Steps / Expected results as
|
|
95
|
+
// the delivery CSV. The cells are composed with the CSV's exact single-vs-
|
|
96
|
+
// numbered join, then stored as ONE array entry — the dashboard XLSX
|
|
97
|
+
// adapter's formatList() (1 entry → raw) reproduces the delivery cell
|
|
98
|
+
// byte-for-byte, and the UI renders the block with pre-wrap.
|
|
99
|
+
if (input.kind === 'api' && input.apiCatalog && hasApiCalls(m.feature.tags)) {
|
|
100
|
+
// Delivery cells substitute {{var}} test-data refs (csv-exporter's localize);
|
|
101
|
+
// scope that to the API branch so non-API snapshot behavior is untouched.
|
|
102
|
+
const localizeApi = (text: string): string => substituteTestDataVars(localize(text), input.testData);
|
|
103
|
+
const formatStepApi = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
104
|
+
localizeApi(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
105
|
+
const joinCell = (lines: string[]): string =>
|
|
106
|
+
lines.length === 0 ? '' :
|
|
107
|
+
lines.length === 1 ? lines[0] :
|
|
108
|
+
lines.map((l, i) => `${i + 1}. ${l}`).join('\n');
|
|
109
|
+
let stepsCell = joinCell(m.resolvedSteps.map(formatStepApi).filter(Boolean));
|
|
110
|
+
let expectedCell = joinCell(m.resolvedExpected.map(formatStepApi).filter(Boolean));
|
|
111
|
+
const apiRequest = formatApiRequest(m.feature.tags, input.apiCatalog, localizeApi);
|
|
112
|
+
if (apiRequest) stepsCell = stepsCell ? `${stepsCell}\n\n${apiRequest}` : apiRequest;
|
|
113
|
+
const apiExpected = formatApiExpected(m.resolvedExpected, m.feature.tags, localizeApi);
|
|
114
|
+
if (apiExpected) expectedCell = apiExpected;
|
|
115
|
+
// Same value-based redaction backstop as the CSV path.
|
|
116
|
+
const secrets = collectSecretValues(input.testData);
|
|
117
|
+
if (secrets.length > 0) {
|
|
118
|
+
stepsCell = redactSecrets(stepsCell, secrets);
|
|
119
|
+
expectedCell = redactSecrets(expectedCell, secrets);
|
|
120
|
+
}
|
|
121
|
+
steps = stepsCell ? [stepsCell] : [];
|
|
122
|
+
expectedResults = expectedCell ? [expectedCell] : [];
|
|
123
|
+
}
|
|
86
124
|
// Dashboard modal uses whitespace-pre-wrap — pass '\n' to render one
|
|
87
125
|
// key/value per line instead of the CSV-friendly "; " join.
|
|
88
126
|
const testDataStr = formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n');
|
|
89
127
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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);
|
|
132
|
+
for (const variant of variants) {
|
|
133
|
+
const displayName = `${m.feature.name}${variant.nameSuffix}`;
|
|
134
|
+
const { vpId, category1 } = splitVpAndName(displayName);
|
|
135
|
+
const tcId = generateTcId(input.screen, vpId, fallbackIndex);
|
|
136
|
+
if (!vpId) fallbackIndex++;
|
|
137
|
+
const category2 = mapVpToCategory2(vpId, category1);
|
|
138
|
+
const result = variant.result;
|
|
94
139
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
let status: ScenarioSnapshot['status'];
|
|
141
|
+
let executedDate: string | undefined;
|
|
142
|
+
let errorMessage: string | undefined;
|
|
143
|
+
let tracePath: string | undefined;
|
|
144
|
+
let testEnvironment: string | undefined;
|
|
145
|
+
let testExecutor: string | undefined;
|
|
146
|
+
let type: ScenarioSnapshot['type'];
|
|
102
147
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
status = 'Pending';
|
|
110
|
-
} else {
|
|
148
|
+
if (result) {
|
|
149
|
+
// A result exists → the scenario compiled AND ran; authoritative over the `!m.spec`
|
|
150
|
+
// "not compiled" heuristic. A @cases test title carries a `— <label>` suffix, so
|
|
151
|
+
// findMatchingSpecTest (base-name match) often leaves m.spec null even though rows ran —
|
|
152
|
+
// without checking `result` first, those passed/failed rows were mislabelled N/A / Pending.
|
|
153
|
+
type = baseType;
|
|
111
154
|
status = statusToTestResult(result.status) as ScenarioSnapshot['status'];
|
|
112
155
|
executedDate = formatExecutedDate(result.startTime);
|
|
113
156
|
if (result.error) {
|
|
@@ -126,31 +169,39 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
126
169
|
if (result.tracePath) tracePath = result.tracePath;
|
|
127
170
|
testEnvironment = formatTestEnvironment(input.env.baseURL, input.featurePath, result.projectName || input.env.projectName);
|
|
128
171
|
testExecutor = input.env.executor;
|
|
172
|
+
} else if (!m.spec) {
|
|
173
|
+
// No result AND no compiled test → Manual if @manual, else genuinely not compiled.
|
|
174
|
+
type = baseType === 'Manual' ? 'Manual' : 'Not compiled';
|
|
175
|
+
status = baseType === 'Manual' ? 'Pending' : 'N/A';
|
|
176
|
+
} else {
|
|
177
|
+
// Compiled but no execution record → Pending.
|
|
178
|
+
type = baseType;
|
|
179
|
+
status = 'Pending';
|
|
129
180
|
}
|
|
130
|
-
}
|
|
131
181
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
182
|
+
scenarios.push({
|
|
183
|
+
tcId,
|
|
184
|
+
category1,
|
|
185
|
+
category2,
|
|
186
|
+
category3: input.featureName,
|
|
187
|
+
category4: input.screen,
|
|
188
|
+
featureBaseName: input.featureBaseName ?? input.screen,
|
|
189
|
+
vpId,
|
|
190
|
+
priority,
|
|
191
|
+
type,
|
|
192
|
+
status,
|
|
193
|
+
tags: m.feature.tags,
|
|
194
|
+
precondition,
|
|
195
|
+
testData: testDataStr,
|
|
196
|
+
steps,
|
|
197
|
+
expectedResults,
|
|
198
|
+
executedDate,
|
|
199
|
+
errorMessage,
|
|
200
|
+
tracePath,
|
|
201
|
+
testEnvironment,
|
|
202
|
+
testExecutor,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
154
205
|
}
|
|
155
206
|
|
|
156
207
|
return {
|
|
@@ -3,8 +3,35 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
6
7
|
import { PlaywrightResult } from './types';
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the results.json file for a unit — shared by the delivery CLI and
|
|
11
|
+
* the dashboard snapshot builder so both surfaces resolve execution status
|
|
12
|
+
* from the SAME file (they drifted: the dashboard only looked for the
|
|
13
|
+
* per-feature file and showed every scenario Pending on projects whose
|
|
14
|
+
* playwright.config writes only the default global test-results/results.json).
|
|
15
|
+
*
|
|
16
|
+
* Candidate order (first existing wins):
|
|
17
|
+
* 1. <genDir>/<featureBaseName>-test-result.<SUNGEN_ENV>.json (env runs)
|
|
18
|
+
* 2. <genDir>/<featureBaseName>-test-result.json (per-feature reporter)
|
|
19
|
+
* 3. <cwd>/test-results/results.<SUNGEN_ENV>.json
|
|
20
|
+
* 4. <cwd>/test-results/results.json (playwright default)
|
|
21
|
+
*/
|
|
22
|
+
export function resolveResultsPath(cwd: string, genDir: string, featureBaseName: string): string | null {
|
|
23
|
+
const env = process.env.SUNGEN_ENV;
|
|
24
|
+
const candidates: string[] = [];
|
|
25
|
+
if (env) candidates.push(path.join(genDir, `${featureBaseName}-test-result.${env}.json`));
|
|
26
|
+
candidates.push(path.join(genDir, `${featureBaseName}-test-result.json`));
|
|
27
|
+
if (env) candidates.push(path.join(cwd, 'test-results', `results.${env}.json`));
|
|
28
|
+
candidates.push(path.join(cwd, 'test-results', 'results.json'));
|
|
29
|
+
for (const c of candidates) {
|
|
30
|
+
if (fs.existsSync(c)) return c;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
/**
|
|
9
36
|
* Shape of the JSON reporter output we care about.
|
|
10
37
|
* See https://playwright.dev/docs/test-reporters#json-reporter
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result-variant resolution — the single seam the CSV (delivery) and JSON (dashboard) exporters
|
|
3
|
+
* share so they can never drift on @cases handling again (they did: the dashboard mislabelled
|
|
4
|
+
* every @cases row while delivery expanded them).
|
|
5
|
+
*/
|
|
6
|
+
import { MergedScenario } from './scenario-merger';
|
|
7
|
+
import { PlaywrightResult } from './types';
|
|
8
|
+
|
|
9
|
+
export interface ResultVariant {
|
|
10
|
+
/** Appended to the scenario display name: `''` for a plain scenario, ` — <label>` per executed
|
|
11
|
+
* @cases dataset row (so each row gets a distinct category1). */
|
|
12
|
+
nameSuffix: string;
|
|
13
|
+
result?: PlaywrightResult;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the execution-result variant(s) for a merged scenario.
|
|
18
|
+
*
|
|
19
|
+
* A `@cases:<dataset>` scenario compiles to one `test()` per dataset row, whose Playwright title is
|
|
20
|
+
* `<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.
|
|
24
|
+
*
|
|
25
|
+
* NOTE: this deliberately does NOT gate on `m.spec`. For @cases the compiled test title carries a
|
|
26
|
+
* `— ${__row.__label}` suffix, so `findMatchingSpecTest` (base-name match) often leaves `m.spec`
|
|
27
|
+
* null even though the rows ran; the marker match still finds their results. Callers must let a
|
|
28
|
+
* present `result` win over the "not compiled" (`!m.spec`) heuristic.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveResultVariants(
|
|
31
|
+
m: MergedScenario,
|
|
32
|
+
results: Map<string, PlaywrightResult> | null,
|
|
33
|
+
): ResultVariant[] {
|
|
34
|
+
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 }));
|
|
40
|
+
}
|
|
41
|
+
return [{ nameSuffix: '' }];
|
|
42
|
+
}
|
|
43
|
+
if (results && m.spec) {
|
|
44
|
+
return [{ nameSuffix: '', result: results.get(m.spec.testTitle) }];
|
|
45
|
+
}
|
|
46
|
+
return [{ nameSuffix: '' }];
|
|
47
|
+
}
|