@sun-asterisk/sungen 3.2.6 → 3.2.8
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 +25 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +30 -47
- 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/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +80 -4
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +6 -1
- package/dist/exporters/csv-exporter.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 +34 -2
- 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/types.d.ts +5 -0
- package/dist/exporters/types.d.ts.map +1 -1
- 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 -595
- 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/playwright/templates/steps/assertions/column-cell-assertion.hbs +3 -3
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +2 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +2 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +2 -2
- package/dist/generators/test-generator/step-mapper.d.ts +6 -2
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +10 -6
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +3 -0
- package/dist/orchestrator/templates/specs-api.d.ts +14 -2
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +38 -14
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +43 -14
- package/package.json +3 -3
- package/src/cli/commands/delivery.ts +32 -46
- 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/api-testcase-formatter.ts +77 -4
- package/src/exporters/csv-exporter.ts +6 -1
- package/src/exporters/json-exporter.ts +46 -4
- package/src/exporters/playwright-report-parser.ts +27 -0
- package/src/exporters/types.ts +5 -0
- package/src/exporters/xlsx-exporter.ts +34 -658
- package/src/exporters/xlsx-report-builder.ts +806 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +3 -3
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +2 -2
- package/src/generators/test-generator/step-mapper.ts +10 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +3 -0
- package/src/orchestrator/templates/specs-api.ts +43 -14
- 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
|
+
}
|
|
@@ -82,8 +82,17 @@ export function formatApiRequest(
|
|
|
82
82
|
const method = String(entry.method ?? '').toUpperCase() || '—';
|
|
83
83
|
const api = entry.path ? resolve(String(entry.path)) : '—';
|
|
84
84
|
const header = formatHeaders(entry, call.args, localize);
|
|
85
|
-
const
|
|
86
|
-
|
|
85
|
+
const lines = [`Method: ${method}`, `API: ${api}`, `Header: ${header}`];
|
|
86
|
+
// A file upload carries its parts in `files:` / `bodyFile:`, NOT `body:` — render them so the
|
|
87
|
+
// delivery Steps reflect the actual request (without this, fields like `gift_image` vanished from
|
|
88
|
+
// the export even though the compiled .spec.ts sent them).
|
|
89
|
+
const filesStr = formatFiles(entry.files, call.args, localize);
|
|
90
|
+
const bodyFileStr = formatBodyFile(entry.bodyFile, call.args, localize);
|
|
91
|
+
// Body is scalar fields; a pure file upload may have none → still show `Body: —` for a stable shape.
|
|
92
|
+
lines.push(`Body: ${formatBody(entry.body, call.args, localize)}`);
|
|
93
|
+
if (filesStr) lines.push(`Files: ${filesStr}`);
|
|
94
|
+
if (bodyFileStr) lines.push(`Body file: ${bodyFileStr}`);
|
|
95
|
+
return lines.join('\n');
|
|
87
96
|
});
|
|
88
97
|
|
|
89
98
|
if (blocks.length === 1) return blocks[0];
|
|
@@ -180,8 +189,14 @@ function formatHeaders(
|
|
|
180
189
|
const parts = Object.entries(explicit).map(([k, v]) =>
|
|
181
190
|
isSensitiveHeader(k) ? `${k}: ${maskHeaderValue(String(v))}` : `${k}: ${localize(bindParams(String(v), args))}`,
|
|
182
191
|
);
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
// Content-Type the request actually sends (Playwright sets it at runtime): a `files:` block is always
|
|
193
|
+
// multipart/form-data (even with `encoding` omitted); a `bodyFile:` sends the file's own media type;
|
|
194
|
+
// otherwise it follows the body encoding. Only shown when the request carries a body/file and the
|
|
195
|
+
// catalog didn't already declare a Content-Type.
|
|
196
|
+
if (!hasContentType) {
|
|
197
|
+
const ct = uploadContentType(entry);
|
|
198
|
+
if (ct) parts.push(`Content-Type: ${ct}`);
|
|
199
|
+
else if (entry.body !== undefined && entry.body !== null) parts.push(`Content-Type: ${contentTypeFor(entry.encoding)}`);
|
|
185
200
|
}
|
|
186
201
|
return parts.length > 0 ? parts.join('\n') : '—';
|
|
187
202
|
}
|
|
@@ -192,6 +207,14 @@ function contentTypeFor(encoding?: string): string {
|
|
|
192
207
|
return 'application/json';
|
|
193
208
|
}
|
|
194
209
|
|
|
210
|
+
/** Content-Type implied by a file upload, or '' when the entry sends no file. */
|
|
211
|
+
function uploadContentType(entry: ApiCatalogEntry): string {
|
|
212
|
+
if (entry.files && typeof entry.files === 'object' && Object.keys(entry.files).length) return 'multipart/form-data';
|
|
213
|
+
const bf = normalizeFileSpec(entry.bodyFile);
|
|
214
|
+
if (bf) return bf.mimeType || 'application/octet-stream';
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
|
|
195
218
|
/**
|
|
196
219
|
* Compose the Body cell, masking credential fields. When the body is a flat object
|
|
197
220
|
* with a sensitive key (password/token/secret/…), each value is rendered per-key so
|
|
@@ -217,6 +240,56 @@ function isPlainObject(v: unknown): boolean {
|
|
|
217
240
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
218
241
|
}
|
|
219
242
|
|
|
243
|
+
interface NormalizedFileSpec { path: string; mimeType?: string; filename?: string; name?: string }
|
|
244
|
+
|
|
245
|
+
/** Normalize a raw file spec (string shorthand or object) → a typed spec, or undefined if invalid. */
|
|
246
|
+
function normalizeFileSpec(raw: unknown): NormalizedFileSpec | undefined {
|
|
247
|
+
if (typeof raw === 'string') return { path: raw };
|
|
248
|
+
if (isPlainObject(raw)) {
|
|
249
|
+
const o = raw as Record<string, unknown>;
|
|
250
|
+
if (typeof o.path === 'string') {
|
|
251
|
+
return {
|
|
252
|
+
path: o.path,
|
|
253
|
+
mimeType: typeof o.mimeType === 'string' ? o.mimeType : undefined,
|
|
254
|
+
filename: typeof o.filename === 'string' ? o.filename : undefined,
|
|
255
|
+
name: typeof o.name === 'string' ? o.name : undefined,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Render one file part: `field: <path> (<mimeType>)`, with `:param`/`{{var}}` resolved. */
|
|
263
|
+
function renderFilePart(field: string, spec: NormalizedFileSpec, args: Record<string, string>, localize: (t: string) => string): string {
|
|
264
|
+
const p = localize(bindParams(spec.path, args));
|
|
265
|
+
return `${spec.name ?? field}: ${p}${spec.mimeType ? ` (${spec.mimeType})` : ''}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Render the `files:` block for the Steps cell. A field value may be a single spec, its string
|
|
270
|
+
* shorthand, or an array (multiple files under one field name) — every part is rendered. Multiple
|
|
271
|
+
* parts are comma-joined. Returns '' when there is no file field.
|
|
272
|
+
*/
|
|
273
|
+
function formatFiles(files: unknown, args: Record<string, string>, localize: (t: string) => string): string {
|
|
274
|
+
if (!isPlainObject(files)) return '';
|
|
275
|
+
const parts: string[] = [];
|
|
276
|
+
for (const [field, raw] of Object.entries(files as Record<string, unknown>)) {
|
|
277
|
+
for (const s of Array.isArray(raw) ? raw : [raw]) {
|
|
278
|
+
const spec = normalizeFileSpec(s);
|
|
279
|
+
if (spec) parts.push(renderFilePart(field, spec, args, localize));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return parts.join(', ');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Render the `bodyFile:` (raw-binary body) for the Steps cell, or '' when absent. */
|
|
286
|
+
function formatBodyFile(bodyFile: unknown, args: Record<string, string>, localize: (t: string) => string): string {
|
|
287
|
+
const spec = normalizeFileSpec(bodyFile);
|
|
288
|
+
if (!spec) return '';
|
|
289
|
+
const p = localize(bindParams(spec.path, args));
|
|
290
|
+
return `${p} (${spec.mimeType || 'application/octet-stream'})`;
|
|
291
|
+
}
|
|
292
|
+
|
|
220
293
|
/**
|
|
221
294
|
* Credential name matcher (substring, not anchored) for both body-field keys and
|
|
222
295
|
* test-data keys — catches variants like confirmPassword / new_password /
|
|
@@ -183,7 +183,12 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
183
183
|
steps,
|
|
184
184
|
expectedResults,
|
|
185
185
|
priority,
|
|
186
|
-
|
|
186
|
+
// An execution `result` proves the scenario compiled AND ran — it MUST win over the
|
|
187
|
+
// `!m.spec` "not compiled" heuristic, exactly as the testResult branch above does. A @cases
|
|
188
|
+
// scenario compiles to per-row titles with a `— <label>` suffix, so findMatchingSpecTest
|
|
189
|
+
// (base-name match) often leaves m.spec null even though the row ran; without checking result
|
|
190
|
+
// here, those already-passed/failed rows were mislabelled "Not compiled".
|
|
191
|
+
testcaseType: (m.spec || result) ? testcaseType : testcaseType === 'Manual' ? 'Manual' : 'Not compiled',
|
|
187
192
|
testResult,
|
|
188
193
|
executedDate,
|
|
189
194
|
testExecutor: executor,
|
|
@@ -15,12 +15,13 @@ 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';
|
|
25
26
|
import { resolveResultVariants } from './result-variants';
|
|
26
27
|
import {
|
|
@@ -53,6 +54,15 @@ export interface BuildScreenSnapshotInput {
|
|
|
53
54
|
* link" instead of the VI key "Click [VỀ KUDOS] link".
|
|
54
55
|
*/
|
|
55
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>;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
/**
|
|
@@ -77,8 +87,40 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
77
87
|
: text;
|
|
78
88
|
const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
79
89
|
localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|
|
82
124
|
// Dashboard modal uses whitespace-pre-wrap — pass '\n' to render one
|
|
83
125
|
// key/value per line instead of the CSV-friendly "; " join.
|
|
84
126
|
const testDataStr = formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n');
|
|
@@ -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
|
package/src/exporters/types.ts
CHANGED
|
@@ -177,5 +177,10 @@ export interface ApiCatalogEntry {
|
|
|
177
177
|
* Absent/`json` → application/json, `form` → application/x-www-form-urlencoded,
|
|
178
178
|
* `multipart` → multipart/form-data. */
|
|
179
179
|
encoding?: 'json' | 'form' | 'multipart';
|
|
180
|
+
/** Multipart file parts (multipart/form-data). A field value may be a single file spec, its string
|
|
181
|
+
* shorthand, or an array of specs (multiple files under one field name). Typed loosely — guard before use. */
|
|
182
|
+
files?: Record<string, unknown>;
|
|
183
|
+
/** Raw-binary body: the request body IS the file's bytes. Typed loosely — guard before use. */
|
|
184
|
+
bodyFile?: unknown;
|
|
180
185
|
expect?: { status?: number | string };
|
|
181
186
|
}
|