@sun-asterisk/sungen 3.2.3 → 3.2.4
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/feedback.d.ts +11 -0
- package/dist/cli/commands/feedback.d.ts.map +1 -1
- package/dist/cli/commands/feedback.js +154 -57
- package/dist/cli/commands/feedback.js.map +1 -1
- package/dist/cli/commands/login.d.ts +19 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +76 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/qa-feedback.d.ts +14 -0
- package/dist/cli/commands/qa-feedback.d.ts.map +1 -0
- package/dist/cli/commands/qa-feedback.js +83 -0
- package/dist/cli/commands/qa-feedback.js.map +1 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +108 -33
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +23 -23
- package/dist/dashboard/types.d.ts +4 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +11 -4
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +4 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +7 -2
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +11 -3
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +21 -2
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +52 -12
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +50 -2
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/spec-parser.js +1 -1
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/exporters/test-data-resolver.d.ts +7 -0
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +13 -0
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +6 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +131 -42
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +15 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +33 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +10 -5
- package/dist/harness/script-check.js.map +1 -1
- package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
- package/dist/orchestrator/ai-skills/writer.js +46 -4
- package/dist/orchestrator/ai-skills/writer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +7 -0
- package/dist/orchestrator/templates/ai-src/commands/feedback.md +6 -6
- package/dist/orchestrator/templates/ai-src/commands/locale.md +26 -4
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
- package/dist/orchestrator/templates/wdio.conf.ts +39 -3
- package/dist/tools/sungen-api/edit-in-editor.d.ts +15 -0
- package/dist/tools/sungen-api/edit-in-editor.d.ts.map +1 -0
- package/dist/tools/sungen-api/edit-in-editor.js +85 -0
- package/dist/tools/sungen-api/edit-in-editor.js.map +1 -0
- package/dist/tools/sungen-api/feedback-client.d.ts +24 -0
- package/dist/tools/sungen-api/feedback-client.d.ts.map +1 -0
- package/dist/tools/sungen-api/feedback-client.js +53 -0
- package/dist/tools/sungen-api/feedback-client.js.map +1 -0
- package/dist/tools/sungen-api/read-stdin.d.ts +6 -0
- package/dist/tools/sungen-api/read-stdin.d.ts.map +1 -0
- package/dist/tools/sungen-api/read-stdin.js +19 -0
- package/dist/tools/sungen-api/read-stdin.js.map +1 -0
- package/dist/tools/sungen-auth/api-base-url.d.ts +14 -0
- package/dist/tools/sungen-auth/api-base-url.d.ts.map +1 -0
- package/dist/tools/sungen-auth/api-base-url.js +28 -0
- package/dist/tools/sungen-auth/api-base-url.js.map +1 -0
- package/dist/tools/sungen-auth/credentials-store.d.ts +19 -0
- package/dist/tools/sungen-auth/credentials-store.d.ts.map +1 -0
- package/dist/tools/sungen-auth/credentials-store.js +94 -0
- package/dist/tools/sungen-auth/credentials-store.js.map +1 -0
- package/dist/tools/sungen-auth/login-flow.d.ts +33 -0
- package/dist/tools/sungen-auth/login-flow.d.ts.map +1 -0
- package/dist/tools/sungen-auth/login-flow.js +107 -0
- package/dist/tools/sungen-auth/login-flow.js.map +1 -0
- package/dist/tools/sungen-auth/loopback-server.d.ts +20 -0
- package/dist/tools/sungen-auth/loopback-server.d.ts.map +1 -0
- package/dist/tools/sungen-auth/loopback-server.js +132 -0
- package/dist/tools/sungen-auth/loopback-server.js.map +1 -0
- package/dist/tools/sungen-auth/open-browser.d.ts +10 -0
- package/dist/tools/sungen-auth/open-browser.d.ts.map +1 -0
- package/dist/tools/sungen-auth/open-browser.js +43 -0
- package/dist/tools/sungen-auth/open-browser.js.map +1 -0
- package/dist/tools/sungen-auth/pkce.d.ts +13 -0
- package/dist/tools/sungen-auth/pkce.d.ts.map +1 -0
- package/dist/tools/sungen-auth/pkce.js +27 -0
- package/dist/tools/sungen-auth/pkce.js.map +1 -0
- package/dist/tools/sungen-auth/sungen-auth-types.d.ts +41 -0
- package/dist/tools/sungen-auth/sungen-auth-types.d.ts.map +1 -0
- package/dist/tools/sungen-auth/sungen-auth-types.js +7 -0
- package/dist/tools/sungen-auth/sungen-auth-types.js.map +1 -0
- package/package.json +3 -3
- package/src/cli/commands/feedback.ts +123 -51
- package/src/cli/commands/login.ts +75 -0
- package/src/cli/commands/qa-feedback.ts +77 -0
- package/src/cli/index.ts +4 -0
- package/src/dashboard/snapshot-builder.ts +100 -27
- package/src/dashboard/templates/index.html +23 -23
- package/src/dashboard/types.ts +5 -1
- package/src/exporters/csv-exporter.ts +10 -4
- package/src/exporters/feature-parser.ts +7 -2
- package/src/exporters/json-exporter.ts +11 -3
- package/src/exporters/playwright-report-parser.ts +50 -11
- package/src/exporters/scenario-merger.ts +49 -3
- package/src/exporters/spec-parser.ts +1 -1
- package/src/exporters/test-data-resolver.ts +13 -0
- package/src/exporters/types.ts +6 -0
- package/src/exporters/xlsx-exporter.ts +155 -49
- package/src/generators/gherkin-parser/index.ts +44 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
- package/src/harness/script-check.ts +10 -5
- package/src/orchestrator/ai-skills/writer.ts +39 -4
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
- package/src/orchestrator/templates/ai-src/commands/create-test.md +7 -0
- package/src/orchestrator/templates/ai-src/commands/feedback.md +6 -6
- package/src/orchestrator/templates/ai-src/commands/locale.md +26 -4
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
- package/src/orchestrator/templates/wdio.conf.ts +39 -3
- package/src/tools/sungen-api/edit-in-editor.ts +48 -0
- package/src/tools/sungen-api/feedback-client.ts +62 -0
- package/src/tools/sungen-api/read-stdin.ts +15 -0
- package/src/tools/sungen-auth/api-base-url.ts +23 -0
- package/src/tools/sungen-auth/credentials-store.ts +57 -0
- package/src/tools/sungen-auth/login-flow.ts +135 -0
- package/src/tools/sungen-auth/loopback-server.ts +113 -0
- package/src/tools/sungen-auth/open-browser.ts +42 -0
- package/src/tools/sungen-auth/pkce.ts +27 -0
- package/src/tools/sungen-auth/sungen-auth-types.ts +45 -0
package/src/dashboard/types.ts
CHANGED
|
@@ -41,12 +41,14 @@ export interface AggregateSummary {
|
|
|
41
41
|
export interface ScreenSnapshot {
|
|
42
42
|
name: string; // "kudos" or "flow/checkout" — the screen DIRECTORY name
|
|
43
43
|
isFlow: boolean;
|
|
44
|
+
platform?: 'web' | 'android' | 'ios' | 'mobile'; // 'mobile' = @platform:mobile screen (per-OS features below)
|
|
44
45
|
featureName: string; // name of the primary (first) feature; kept for backwards compat
|
|
45
46
|
featurePath?: string; // entry-point path of the primary feature
|
|
46
47
|
specLink?: string; // relative path to requirements/spec.md
|
|
47
48
|
summary: ScreenSummaryStats; // rollup across all features
|
|
48
49
|
scenarios: ScenarioSnapshot[]; // flat list across all features (each carries its `featureBaseName`)
|
|
49
50
|
features?: FeatureSnapshot[]; // present when a screen has 1+ .feature files; UI groups by this
|
|
51
|
+
executedPlatforms?: ('android' | 'ios')[]; // OSes that produced a result file this run (mobile only)
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -67,7 +69,9 @@ export interface FeatureSnapshot {
|
|
|
67
69
|
specLink?: string; // relative path to requirements/spec.md, when present
|
|
68
70
|
summary: ScreenSummaryStats; // BASE locale rollup — back-compat for non-locale consumers
|
|
69
71
|
scenarios: ScenarioSnapshot[]; // BASE locale scenarios
|
|
70
|
-
locales?: LocaleSnapshot[]; //
|
|
72
|
+
locales?: LocaleSnapshot[]; // locale overlays WITHIN this entry (base + at least one overlay)
|
|
73
|
+
platform?: 'android' | 'ios'; // one OS run of a @platform:mobile feature (separate entry per OS)
|
|
74
|
+
executedPlatforms?: ('android' | 'ios')[]; // OSes that produced a result file this run (mobile only)
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
/**
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
splitVpAndName,
|
|
17
17
|
} from './feature-parser';
|
|
18
18
|
import { formatFeatureSteps, formatPrecondition, keepActor, stripActor } from './step-formatter';
|
|
19
|
-
import { formatTestData } from './test-data-resolver';
|
|
19
|
+
import { formatTestData, substituteTestDataVars } from './test-data-resolver';
|
|
20
20
|
import {
|
|
21
21
|
formatExecutedDate,
|
|
22
22
|
formatNote,
|
|
@@ -83,10 +83,16 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
83
83
|
// Pre-condition is auth-only. Steps section = Background + @extend base
|
|
84
84
|
// (entire) + own pre-Then. Expected = own's Then-onwards only.
|
|
85
85
|
const precondition = formatPrecondition(authRole);
|
|
86
|
-
const localize = (text: string): string =>
|
|
87
|
-
|
|
86
|
+
const localize = (text: string): string => {
|
|
87
|
+
// 1. resolve [Reference] selector keys → their label/value; 2. resolve {{var}} test-data
|
|
88
|
+
// placeholders → concrete values, so Steps / Expected results show what QA actually verifies
|
|
89
|
+
// (e.g. `text contains {{app_version}}` → `text contains Version 1.0.0`) instead of the raw
|
|
90
|
+
// template. An unknown {{var}} is left literal.
|
|
91
|
+
const withKeys = input.selectorKeyMap
|
|
88
92
|
? substituteSelectorKeysInStep(text, input.selectorKeyMap, input.testData)
|
|
89
93
|
: text;
|
|
94
|
+
return substituteTestDataVars(withKeys, input.testData);
|
|
95
|
+
};
|
|
90
96
|
const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
91
97
|
localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
92
98
|
const stepLines = m.resolvedSteps.map(formatStep).filter(Boolean);
|
|
@@ -127,7 +133,7 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
127
133
|
} else {
|
|
128
134
|
testResult = statusToTestResult(result.status);
|
|
129
135
|
executedDate = formatExecutedDate(result.startTime);
|
|
130
|
-
note = formatNote(result);
|
|
136
|
+
note = formatNote(result, expectedResults);
|
|
131
137
|
environment = formatEnv(input.env.baseURL, input.featurePath, result.projectName || input.env.projectName);
|
|
132
138
|
executor = input.env.executor;
|
|
133
139
|
}
|
|
@@ -90,6 +90,7 @@ export function parseFeatureMetadata(featureFilePath: string): FeatureMetadata {
|
|
|
90
90
|
rawWhenSteps: when,
|
|
91
91
|
rawThenSteps: then,
|
|
92
92
|
orderedSteps: ordered,
|
|
93
|
+
comments: sc.comments,
|
|
93
94
|
};
|
|
94
95
|
});
|
|
95
96
|
|
|
@@ -132,9 +133,13 @@ export function extractPriority(tags: string[]): string {
|
|
|
132
133
|
|
|
133
134
|
/**
|
|
134
135
|
* Extract testcase type from scenario tags.
|
|
136
|
+
* Matches the bare `@manual` AND the reason-coded form `@manual:M1`..`@manual:M9`
|
|
137
|
+
* (the canonical way to tag manual scenarios). Without the `startsWith` check a
|
|
138
|
+
* `@manual:M1` scenario was misclassified as `Auto` → rendered "Not compiled"/"N/A"
|
|
139
|
+
* in the report instead of "Manual"/"Pending".
|
|
135
140
|
*/
|
|
136
141
|
export function extractTestcaseType(tags: string[]): 'Auto' | 'Manual' {
|
|
137
|
-
return tags.
|
|
142
|
+
return tags.some((t) => t === '@manual' || t.startsWith('@manual:')) ? 'Manual' : 'Auto';
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
/**
|
|
@@ -153,7 +158,7 @@ export function extractAuthRole(tags: string[]): string | null {
|
|
|
153
158
|
* { vpId: "VP-UI-001", category1: "Modal displays all required fields" }
|
|
154
159
|
*/
|
|
155
160
|
export function splitVpAndName(scenarioName: string): { vpId?: string; category1: string } {
|
|
156
|
-
const match = scenarioName.match(/^(VP-[A-Z]
|
|
161
|
+
const match = scenarioName.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)\s+(.+)$/);
|
|
157
162
|
if (match) {
|
|
158
163
|
return { vpId: match[1], category1: match[2] };
|
|
159
164
|
}
|
|
@@ -111,9 +111,17 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
111
111
|
status = statusToTestResult(result.status) as ScenarioSnapshot['status'];
|
|
112
112
|
executedDate = formatExecutedDate(result.startTime);
|
|
113
113
|
if (result.error) {
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
114
|
+
// Keep the FULL assertion message on one line. Do NOT use split('\n')[0]: a WDIO
|
|
115
|
+
// selector value can itself contain a newline (a Flutter composite accessibility-id
|
|
116
|
+
// like `~Share with Friends\nSpread the word`), which would truncate the message
|
|
117
|
+
// mid-selector. Drop the stack trace (frames start with "at "), collapse the remaining
|
|
118
|
+
// newlines (incl. the Expected/Received lines) to spaces, and strip a leading "Error:"
|
|
119
|
+
// so consumers (dashboard / CSV `Note`) can prepend their own label.
|
|
120
|
+
errorMessage = String(result.error)
|
|
121
|
+
.split(/\n\s*at\s/)[0]
|
|
122
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
123
|
+
.trim()
|
|
124
|
+
.replace(/^Error:\s*/i, '');
|
|
117
125
|
}
|
|
118
126
|
if (result.tracePath) tracePath = result.tracePath;
|
|
119
127
|
testEnvironment = formatTestEnvironment(input.env.baseURL, input.featurePath, result.projectName || input.env.projectName);
|
|
@@ -192,22 +192,61 @@ export function formatExecutedDate(startTime: string | undefined): string {
|
|
|
192
192
|
return `${dd}/${mm}/${yyyy}`;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
const NOTE_FIELD_MAX = 300;
|
|
196
|
+
const clip = (s: string): string => {
|
|
197
|
+
const t = s.replace(/\s*\n\s*/g, ' ').trim();
|
|
198
|
+
return t.length > NOTE_FIELD_MAX ? t.slice(0, NOTE_FIELD_MAX - 1) + '…' : t;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Split an assertion failure message into its Expected + Received values, so QA can compare.
|
|
203
|
+
* Both frameworks embed them in the error message: Playwright — `Expected string: "…"` /
|
|
204
|
+
* `Received string: "…"` (also `Expected pattern/substring/value`, `Received value`); expect-
|
|
205
|
+
* webdriverio — `Expected: …` / `Received: …`. `(.+)` (no /s flag) captures to end-of-line, so a
|
|
206
|
+
* multi-line stack after the value is not swallowed. Undefined when the message isn't an assertion
|
|
207
|
+
* (e.g. element-not-found / timeout) — then there is no runtime Expected to show.
|
|
208
|
+
*/
|
|
209
|
+
export function parseExpectedReceived(raw: string): { expected?: string; received?: string } {
|
|
210
|
+
const grab = (re: RegExp): string | undefined => { const m = raw.match(re); return m ? clip(m[1]) : undefined; };
|
|
211
|
+
return {
|
|
212
|
+
expected: grab(/\bExpected(?:\s+[\w-]+)?:\s*(.+)/i),
|
|
213
|
+
received: grab(/\bReceived(?:\s+[\w-]+)?:\s*(.+)/i),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
195
217
|
/**
|
|
196
|
-
* Format
|
|
218
|
+
* Format the Note column for a FAILED test case (returns '' for any non-failed status — so only
|
|
219
|
+
* `Test Result = Failed` rows get a Note). Emits the two fields QA compares:
|
|
220
|
+
* • Expected result: the asserted value parsed from the error, or — when the failure isn't an
|
|
221
|
+
* assertion (timeout / element-not-found, which carry no Expected/Received) — the scenario's
|
|
222
|
+
* expected (`expectedFallback`, i.e. the "Expected results" column / Then steps), so QA always
|
|
223
|
+
* sees what SHOULD have happened next to the actual.
|
|
224
|
+
* • Actual result: the Received value, or the raw error.
|
|
225
|
+
* Plus the trace path.
|
|
197
226
|
*/
|
|
198
|
-
export function formatNote(result: PlaywrightResult | undefined): string {
|
|
227
|
+
export function formatNote(result: PlaywrightResult | undefined, expectedFallback = ''): string {
|
|
199
228
|
if (!result) return '';
|
|
200
229
|
if (result.status !== 'failed' && result.status !== 'timedOut') return '';
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
const
|
|
204
|
-
const
|
|
230
|
+
// Full assertion message with stack frames dropped (frames start with "at ") + a leading `Error:`
|
|
231
|
+
// stripped. Keep newlines here so parseExpectedReceived can delimit the Expected/Received lines.
|
|
232
|
+
const raw = (result.error || '').split(/\n\s*at\s/)[0].replace(/^Error:\s*/i, '').trim();
|
|
233
|
+
const { expected, received } = parseExpectedReceived(raw);
|
|
234
|
+
const actual = received || clip(raw); // received value, else the raw error
|
|
235
|
+
|
|
236
|
+
// Newline-separated bullets so each field lands on its own row when the cell wraps — matches the
|
|
237
|
+
// Test Data column's `• key: value` formatting. Excel/CSV honour `\n` inside quoted cells; the
|
|
238
|
+
// dashboard modal renders with whitespace-pre-wrap.
|
|
205
239
|
const parts: string[] = [];
|
|
206
|
-
|
|
240
|
+
// Expected result: a parsed assertion value stays inline; the scenario expected (Then steps) can be
|
|
241
|
+
// several numbered criteria — put each on its own indented line so the cell reads as a list.
|
|
242
|
+
if (expected) {
|
|
243
|
+
parts.push(`• Expected result: ${expected}`);
|
|
244
|
+
} else {
|
|
245
|
+
const lines = expectedFallback.split(/\r?\n/).map((l) => clip(l)).filter(Boolean);
|
|
246
|
+
if (lines.length === 1) parts.push(`• Expected result: ${lines[0]}`);
|
|
247
|
+
else if (lines.length > 1) parts.push(`• Expected result:\n${lines.map((l) => ` ${l}`).join('\n')}`);
|
|
248
|
+
}
|
|
249
|
+
if (actual) parts.push(`• Actual result: ${actual}`);
|
|
207
250
|
if (result.tracePath) parts.push(`• Trace: ${result.tracePath}`);
|
|
208
|
-
// Newline-separated bullets so each field lands on its own row when the
|
|
209
|
-
// cell wraps — matches the Test Data column's `• key: value` formatting.
|
|
210
|
-
// Excel/CSV honour `\n` inside quoted cells; the dashboard modal already
|
|
211
|
-
// renders this with whitespace-pre-wrap.
|
|
212
251
|
return parts.join('\n');
|
|
213
252
|
}
|
|
@@ -9,7 +9,43 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { FeatureMetadata, OrderedStep, ScenarioMetadata, SpecFileData, SpecTest } from './types';
|
|
12
|
-
import { isStepsBaseScenario, isSampleScaffoldScenario } from './feature-parser';
|
|
12
|
+
import { isStepsBaseScenario, isSampleScaffoldScenario, extractTestcaseType } from './feature-parser';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a @manual scenario's `# Tester verifies:` comment block into procedure vs
|
|
16
|
+
* expected. Each item is a numbered line, optionally prefixed with a label:
|
|
17
|
+
* 1. Setup: … 2. Action: … → procedure (Steps column)
|
|
18
|
+
* 3. Observable: … 4. Oracle: … → expected (Expected Results column)
|
|
19
|
+
* A label matching observ|oracle|expect|result|then|assert → Expected; anything else
|
|
20
|
+
* (Setup/Action/Precondition/unlabelled) → Steps. A non-numbered line following a
|
|
21
|
+
* numbered item is a continuation (e.g. a wrapped URL) and is appended to it. Pre-amble
|
|
22
|
+
* lines (the `MANUAL:` rationale, the `Tester verifies:` header, separators) are ignored.
|
|
23
|
+
*/
|
|
24
|
+
function parseManualComments(comments: string[]): { steps: OrderedStep[]; expected: OrderedStep[] } {
|
|
25
|
+
const steps: OrderedStep[] = [];
|
|
26
|
+
const expected: OrderedStep[] = [];
|
|
27
|
+
let last: OrderedStep | null = null;
|
|
28
|
+
const isExpectedLabel = (l: string) => /observ|oracle|expect|result|then|assert/i.test(l);
|
|
29
|
+
for (const raw of comments) {
|
|
30
|
+
const line = raw.trim();
|
|
31
|
+
if (!line) continue;
|
|
32
|
+
// Section divider — all divider chars (`-----`) OR a titled rule (`--- Foo ---`).
|
|
33
|
+
// A run of 2+ leading divider chars is never a step or a continuation.
|
|
34
|
+
if (/^[-=*_]{2,}/.test(line)) { last = null; continue; }
|
|
35
|
+
const m = line.match(/^\d+[.)]\s*(?:([A-Za-z][A-Za-z /]*?):\s*)?(.+)$/);
|
|
36
|
+
if (m) {
|
|
37
|
+
const label = (m[1] || '').trim();
|
|
38
|
+
const text = label ? `${label}: ${m[2].trim()}` : m[2].trim();
|
|
39
|
+
const step: OrderedStep = { text, bucket: isExpectedLabel(label) ? 'then' : 'when' };
|
|
40
|
+
(step.bucket === 'then' ? expected : steps).push(step);
|
|
41
|
+
last = step;
|
|
42
|
+
} else if (last) {
|
|
43
|
+
last.text += ' ' + line; // continuation of the previous numbered item
|
|
44
|
+
}
|
|
45
|
+
// else: pre-amble before any numbered item (rationale / header) → skip
|
|
46
|
+
}
|
|
47
|
+
return { steps, expected };
|
|
48
|
+
}
|
|
13
49
|
|
|
14
50
|
export interface MergedScenario {
|
|
15
51
|
/** Scenario metadata from .feature */
|
|
@@ -76,6 +112,15 @@ export function mergeFeatureAndSpec(
|
|
|
76
112
|
const ownPreThen = firstThenIdx >= 0 ? ownOrdered.slice(0, firstThenIdx) : ownOrdered;
|
|
77
113
|
const ownPostThen = firstThenIdx >= 0 ? ownOrdered.slice(firstThenIdx) : [];
|
|
78
114
|
|
|
115
|
+
// A @manual scenario carries no Gherkin steps — its tester procedure lives in the
|
|
116
|
+
// `# Tester verifies:` comment block. Parse it into Steps (Setup/Action) + Expected
|
|
117
|
+
// (Observable/Oracle) so the delivery report shows the real manual procedure, not
|
|
118
|
+
// just the Background nav. No-op for automated scenarios (comments stay notes).
|
|
119
|
+
const manualParsed =
|
|
120
|
+
extractTestcaseType(scenario.tags) === 'Manual' && scenario.comments?.length
|
|
121
|
+
? parseManualComments(scenario.comments)
|
|
122
|
+
: { steps: [] as OrderedStep[], expected: [] as OrderedStep[] };
|
|
123
|
+
|
|
79
124
|
result.push({
|
|
80
125
|
feature: scenario,
|
|
81
126
|
spec: specTest,
|
|
@@ -98,8 +143,9 @@ export function mergeFeatureAndSpec(
|
|
|
98
143
|
...feature.backgroundOrderedSteps,
|
|
99
144
|
...(base?.orderedSteps ?? []),
|
|
100
145
|
...ownPreThen,
|
|
146
|
+
...manualParsed.steps,
|
|
101
147
|
],
|
|
102
|
-
resolvedExpected: ownPostThen,
|
|
148
|
+
resolvedExpected: [...ownPostThen, ...manualParsed.expected],
|
|
103
149
|
});
|
|
104
150
|
}
|
|
105
151
|
|
|
@@ -116,7 +162,7 @@ function findMatchingSpecTest(scenario: ScenarioMetadata, tests: SpecTest[]): Sp
|
|
|
116
162
|
if (exact) return exact;
|
|
117
163
|
|
|
118
164
|
// Fallback: match by VP-ID prefix (ignore trailing text differences)
|
|
119
|
-
const vpMatch = scenario.name.match(/^(VP-[A-Z]
|
|
165
|
+
const vpMatch = scenario.name.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)/);
|
|
120
166
|
if (vpMatch) {
|
|
121
167
|
const vpId = vpMatch[1];
|
|
122
168
|
const byVp = tests.find((t) => t.vpId === vpId);
|
|
@@ -146,7 +146,7 @@ function classifyBlock(block: CodeBlock): 'precondition' | 'step' | 'expectation
|
|
|
146
146
|
* Try to extract VP ID (e.g., VP-UI-001) from the start of a scenario title.
|
|
147
147
|
*/
|
|
148
148
|
function extractVpId(title: string): string | undefined {
|
|
149
|
-
const match = title.match(/^(VP-[A-Z]
|
|
149
|
+
const match = title.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)\b/);
|
|
150
150
|
return match ? match[1] : undefined;
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -133,6 +133,19 @@ function truncate(s: string, max: number): string {
|
|
|
133
133
|
return s.substring(0, max - 1) + '…';
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Resolve `{{var}}` test-data placeholders in a step / expected-result line to their concrete values,
|
|
138
|
+
* so the delivery report shows what QA actually verifies (e.g. `text contains {{app_version}}` →
|
|
139
|
+
* `text contains Version 1.0.0`). Dotted keys (`{{a.b}}`) are supported. A key absent from the
|
|
140
|
+
* test-data map (or resolving to empty) is left as the literal `{{var}}` — never blanked.
|
|
141
|
+
*/
|
|
142
|
+
export function substituteTestDataVars(text: string, testData: Record<string, string>): string {
|
|
143
|
+
return text.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (m, key: string) => {
|
|
144
|
+
const v = testData[key];
|
|
145
|
+
return v === undefined || v === null || v === '' ? m : String(v);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
136
149
|
/**
|
|
137
150
|
* Resolve the test-data file path for a given screen.
|
|
138
151
|
*/
|
package/src/exporters/types.ts
CHANGED
|
@@ -88,6 +88,12 @@ export interface ScenarioMetadata {
|
|
|
88
88
|
* Use this when ordering matters — e.g. mid-scenario `Given` after `When`.
|
|
89
89
|
*/
|
|
90
90
|
orderedSteps: OrderedStep[];
|
|
91
|
+
/**
|
|
92
|
+
* Comment lines inside the scenario body (leading `# ` stripped). For a @manual
|
|
93
|
+
* scenario the tester procedure lives here (as `# Tester verifies:` comments), not
|
|
94
|
+
* as Gherkin steps — the merger parses these into Steps/Expected for the report.
|
|
95
|
+
*/
|
|
96
|
+
comments?: string[];
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
/**
|
|
@@ -30,6 +30,15 @@ const META_FILL = 'FFDCE6F1'; // soft blue for meta rows
|
|
|
30
30
|
const CATEGORY_FILL = 'FFD9D2E9'; // same lavender for group dividers (Accessing / GUI / Function)
|
|
31
31
|
const BORDER_COLOR = 'FFBFBFBF';
|
|
32
32
|
|
|
33
|
+
// Shared masthead + table styling (BM-2-901-13 header band, summary band, Overview table).
|
|
34
|
+
const TIMES = 'Times New Roman';
|
|
35
|
+
const ARIAL_FONT = 'Arial';
|
|
36
|
+
const BLACK = { argb: 'FF000000' };
|
|
37
|
+
const WHITE = { argb: 'FFFFFFFF' };
|
|
38
|
+
const LAVENDER = { argb: 'FFD9D2E9' };
|
|
39
|
+
const thinBlack = { style: 'thin' as const, color: BLACK };
|
|
40
|
+
const allBordersBlack = { top: thinBlack, left: thinBlack, bottom: thinBlack, right: thinBlack };
|
|
41
|
+
|
|
33
42
|
type AnyCell = ExcelJS.Cell;
|
|
34
43
|
type AnyRow = ExcelJS.Row;
|
|
35
44
|
|
|
@@ -96,6 +105,9 @@ export function renderXlsxMultiSheet(sheets: LocaleSheet[]): ExcelJS.Workbook {
|
|
|
96
105
|
const wb = new ExcelJS.Workbook();
|
|
97
106
|
wb.creator = 'sungen delivery';
|
|
98
107
|
wb.created = new Date();
|
|
108
|
+
// Overview first — a per-sheet roll-up (Passed/Failed/Pending/N/A/Remain/Total)
|
|
109
|
+
// plus a grand-total row, so QA sees the whole picture before the detail sheets.
|
|
110
|
+
addOverviewSheet(wb, sheets);
|
|
99
111
|
for (const s of sheets) {
|
|
100
112
|
addTestcaseSheet(wb, s.sheetName, s.summary, s.rows, s.specLink);
|
|
101
113
|
}
|
|
@@ -103,57 +115,23 @@ export function renderXlsxMultiSheet(sheets: LocaleSheet[]): ExcelJS.Workbook {
|
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
/**
|
|
106
|
-
*
|
|
107
|
-
* (
|
|
108
|
-
*
|
|
118
|
+
* Render the shared BM-2-901-13 masthead (rows 1-4) onto a worksheet:
|
|
119
|
+
* Sun* logo (A1:C3), TESTCASE title (D1:F3), No/Version/Issue-Date block
|
|
120
|
+
* (G1:H3), company line (A4:F4), ISO line (G4:H4). Every sheet in a delivery
|
|
121
|
+
* workbook — Overview, Auto, Manual — carries this same header band.
|
|
109
122
|
*/
|
|
110
|
-
function
|
|
123
|
+
function renderReportHeaderBand(
|
|
111
124
|
wb: ExcelJS.Workbook,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
rows: TestCaseRow[],
|
|
115
|
-
specLink: string,
|
|
125
|
+
ws: ExcelJS.Worksheet,
|
|
126
|
+
titleLabel: string,
|
|
116
127
|
): void {
|
|
117
|
-
const ws = wb.addWorksheet(sheetName, {
|
|
118
|
-
views: [{ state: 'frozen', ySplit: 0, xSplit: 0 }],
|
|
119
|
-
});
|
|
120
|
-
|
|
121
128
|
const issueDate = (() => {
|
|
122
129
|
const d = new Date();
|
|
123
130
|
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
|
|
124
131
|
})();
|
|
125
132
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
{ width: 20 }, // A — TC ID (wider to fit long flow IDs like FLOW-KUDO-…)
|
|
129
|
-
{ width: 12.5 }, // B — Screen/Function
|
|
130
|
-
{ width: 15.38 }, // C — Big item
|
|
131
|
-
{ width: 16.25 }, // D — Medium item
|
|
132
|
-
{ width: 17 }, // E — Test Object
|
|
133
|
-
{ width: 25.5 }, // F — Pre-condition
|
|
134
|
-
{ width: 24.88 }, // G — Test Data
|
|
135
|
-
{ width: 24.88 }, // H — Steps
|
|
136
|
-
{ width: 41 }, // I — Expected results
|
|
137
|
-
{ width: 12 }, // J — Priority
|
|
138
|
-
{ width: 13.5 }, // K — Testcase type
|
|
139
|
-
{ width: 15.88 }, // L — Test Result
|
|
140
|
-
{ width: 13.5 }, // M — Executed Date
|
|
141
|
-
{ width: 16.5 }, // N — Test Executor
|
|
142
|
-
{ width: 23 }, // O — Test Environment
|
|
143
|
-
{ width: 29 }, // P — Note
|
|
144
|
-
];
|
|
145
|
-
|
|
146
|
-
// -- Top metadata band (rows 1-4) — 1:1 with template_report.xlsx --
|
|
147
|
-
const TIMES = 'Times New Roman';
|
|
148
|
-
const ARIAL_FONT = 'Arial';
|
|
149
|
-
const BLACK = { argb: 'FF000000' };
|
|
150
|
-
const WHITE = { argb: 'FFFFFFFF' };
|
|
151
|
-
const LAVENDER = { argb: 'FFD9D2E9' };
|
|
152
|
-
const thinBlack = { style: 'thin' as const, color: BLACK };
|
|
153
|
-
const allBordersBlack = { top: thinBlack, left: thinBlack, bottom: thinBlack, right: thinBlack };
|
|
154
|
-
|
|
155
|
-
// Ensure rows 1..8 exist before merging.
|
|
156
|
-
for (let r = 1; r <= 8; r++) ws.getRow(r);
|
|
133
|
+
// Ensure rows 1..4 exist before merging.
|
|
134
|
+
for (let r = 1; r <= 4; r++) ws.getRow(r);
|
|
157
135
|
|
|
158
136
|
ws.mergeCells('A1:C3');
|
|
159
137
|
ws.mergeCells('D1:F3');
|
|
@@ -174,10 +152,6 @@ function addTestcaseSheet(
|
|
|
174
152
|
extension: 'png',
|
|
175
153
|
});
|
|
176
154
|
// Centre a fixed-size 90×51 logo inside A1:C3 using absolute EMU offsets.
|
|
177
|
-
// Col widths (A=20, B=12.5, C=15.38) → pixels via Excel's `w*7+5` rule:
|
|
178
|
-
// A=145, B=92.5, C=112.66 ⇒ A1:C3 ≈ 350 px wide.
|
|
179
|
-
// Left padding for a 90-px image = (350−90)/2 ≈ 130 px = 1,237,500 EMU.
|
|
180
|
-
// 3 default rows × 20 px = 60 px; top padding ≈ 4.5 px ≈ 42,862 EMU.
|
|
181
155
|
ws.addImage(imageId, {
|
|
182
156
|
tl: { nativeCol: 0, nativeColOff: 1237500, nativeRow: 0, nativeRowOff: 42862 } as unknown as ExcelJS.Anchor,
|
|
183
157
|
ext: { width: 90, height: 51 },
|
|
@@ -186,7 +160,6 @@ function addTestcaseSheet(
|
|
|
186
160
|
} catch { /* logo is decorative — never block export */ }
|
|
187
161
|
|
|
188
162
|
// D1 (TESTCASE title)
|
|
189
|
-
const titleLabel = `${summary.screen.toUpperCase()} TESTCASE`;
|
|
190
163
|
const d1 = ws.getCell('D1');
|
|
191
164
|
d1.value = titleLabel;
|
|
192
165
|
d1.font = { bold: true, size: 18, name: TIMES };
|
|
@@ -227,6 +200,134 @@ function addTestcaseSheet(
|
|
|
227
200
|
g4.font = { bold: true, size: 10, name: TIMES };
|
|
228
201
|
g4.alignment = { horizontal: 'right', vertical: 'middle', wrapText: true };
|
|
229
202
|
g4.border = allBordersBlack;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Overview sheet — the first tab in every multi-sheet delivery workbook.
|
|
207
|
+
// Rolls up each testcase sheet's Passed/Failed/Pending/N/A/Remain/Total into
|
|
208
|
+
// one summary table (+ a grand-total row) under the shared masthead. Reads
|
|
209
|
+
// only each sheet's summary — never touches the detail sheets themselves.
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/** Column headers for the Overview roll-up table (cols A..H). */
|
|
213
|
+
const OVERVIEW_HEADERS = ['#', 'Sheet name', 'Passed', 'Failed', 'Pending', 'N/A', 'Remain', 'Total'];
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Prepend an "Overview" worksheet summarizing every sheet in `sheets`.
|
|
217
|
+
*
|
|
218
|
+
* `Remain = Total − Passed − N/A` (i.e. Failed + Pending) so the number
|
|
219
|
+
* reconciles with the "Remaining" cell in each detail sheet's summary band.
|
|
220
|
+
*/
|
|
221
|
+
function addOverviewSheet(wb: ExcelJS.Workbook, sheets: LocaleSheet[]): void {
|
|
222
|
+
const ws = wb.addWorksheet('Overview');
|
|
223
|
+
|
|
224
|
+
// Table starts at column B row 7 (A is a thin left margin). Widths keep the
|
|
225
|
+
// A..H masthead readable AND the count columns tidy.
|
|
226
|
+
ws.columns = [
|
|
227
|
+
{ width: 4 }, // A — left margin
|
|
228
|
+
{ width: 6 }, // B — #
|
|
229
|
+
{ width: 26 }, // C — Sheet name
|
|
230
|
+
{ width: 13 }, // D — Passed
|
|
231
|
+
{ width: 13 }, // E — Failed
|
|
232
|
+
{ width: 13 }, // F — Pending
|
|
233
|
+
{ width: 13 }, // G — N/A
|
|
234
|
+
{ width: 13 }, // H — Remain
|
|
235
|
+
{ width: 13 }, // I — Total
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
const titleScreen = sheets[0]?.summary.screen ?? '';
|
|
239
|
+
renderReportHeaderBand(wb, ws, `${titleScreen.toUpperCase()} TESTCASE`);
|
|
240
|
+
|
|
241
|
+
// Roll-up table: header on row 7, first column B (index 2).
|
|
242
|
+
const HEADER_ROW = 7;
|
|
243
|
+
const FIRST_COL = 2; // column B
|
|
244
|
+
const headerRow = ws.getRow(HEADER_ROW);
|
|
245
|
+
headerRow.height = 20;
|
|
246
|
+
OVERVIEW_HEADERS.forEach((label, i) => {
|
|
247
|
+
const c = headerRow.getCell(FIRST_COL + i);
|
|
248
|
+
c.value = label;
|
|
249
|
+
c.font = { bold: true, size: 10, color: BLACK, name: ARIAL_FONT };
|
|
250
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: LAVENDER };
|
|
251
|
+
c.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
252
|
+
c.border = allBordersBlack;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const totals = { passed: 0, failed: 0, pending: 0, na: 0, remain: 0, total: 0 };
|
|
256
|
+
|
|
257
|
+
const writeDataRow = (rowIdx: number, values: ExcelJS.CellValue[], isTotal: boolean): void => {
|
|
258
|
+
const row = ws.getRow(rowIdx);
|
|
259
|
+
values.forEach((v, i) => {
|
|
260
|
+
const c = row.getCell(FIRST_COL + i);
|
|
261
|
+
c.value = v;
|
|
262
|
+
c.font = { bold: isTotal, size: 10, color: BLACK, name: ARIAL_FONT };
|
|
263
|
+
// The '#' + 'Sheet name' columns read left; the counts centre like the band.
|
|
264
|
+
c.alignment = { horizontal: i <= 1 ? 'left' : 'center', vertical: 'middle' };
|
|
265
|
+
c.border = allBordersBlack;
|
|
266
|
+
if (isTotal) c.fill = { type: 'pattern', pattern: 'solid', fgColor: LAVENDER };
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
let rowIdx = HEADER_ROW + 1;
|
|
271
|
+
sheets.forEach((s, i) => {
|
|
272
|
+
const su = s.summary;
|
|
273
|
+
const remain = Math.max(0, su.total - su.passed - su.na);
|
|
274
|
+
writeDataRow(rowIdx, [i + 1, s.sheetName, su.passed, su.failed, su.pending, su.na, remain, su.total], false);
|
|
275
|
+
totals.passed += su.passed;
|
|
276
|
+
totals.failed += su.failed;
|
|
277
|
+
totals.pending += su.pending;
|
|
278
|
+
totals.na += su.na;
|
|
279
|
+
totals.remain += remain;
|
|
280
|
+
totals.total += su.total;
|
|
281
|
+
rowIdx += 1;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
writeDataRow(
|
|
285
|
+
rowIdx,
|
|
286
|
+
['', 'Total', totals.passed, totals.failed, totals.pending, totals.na, totals.remain, totals.total],
|
|
287
|
+
true,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Append one BM-2-901-13 testcase sheet to the given workbook. Sheet content
|
|
293
|
+
* (layout, summary band, data rows, auto-filter, protection) is identical to
|
|
294
|
+
* the single-sheet `renderXlsx` output — only the sheet name differs.
|
|
295
|
+
*/
|
|
296
|
+
function addTestcaseSheet(
|
|
297
|
+
wb: ExcelJS.Workbook,
|
|
298
|
+
sheetName: string,
|
|
299
|
+
summary: ScreenSummary,
|
|
300
|
+
rows: TestCaseRow[],
|
|
301
|
+
specLink: string,
|
|
302
|
+
): void {
|
|
303
|
+
const ws = wb.addWorksheet(sheetName, {
|
|
304
|
+
views: [{ state: 'frozen', ySplit: 0, xSplit: 0 }],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// -- Column widths matching template_report.xlsx (Sample sheet) --
|
|
308
|
+
ws.columns = [
|
|
309
|
+
{ width: 20 }, // A — TC ID (wider to fit long flow IDs like FLOW-KUDO-…)
|
|
310
|
+
{ width: 12.5 }, // B — Screen/Function
|
|
311
|
+
{ width: 15.38 }, // C — Big item
|
|
312
|
+
{ width: 16.25 }, // D — Medium item
|
|
313
|
+
{ width: 17 }, // E — Test Object
|
|
314
|
+
{ width: 25.5 }, // F — Pre-condition
|
|
315
|
+
{ width: 24.88 }, // G — Test Data
|
|
316
|
+
{ width: 24.88 }, // H — Steps
|
|
317
|
+
{ width: 41 }, // I — Expected results
|
|
318
|
+
{ width: 12 }, // J — Priority
|
|
319
|
+
{ width: 13.5 }, // K — Testcase type
|
|
320
|
+
{ width: 15.88 }, // L — Test Result
|
|
321
|
+
{ width: 13.5 }, // M — Executed Date
|
|
322
|
+
{ width: 16.5 }, // N — Test Executor
|
|
323
|
+
{ width: 23 }, // O — Test Environment
|
|
324
|
+
{ width: 29 }, // P — Note
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
// -- Top metadata band (rows 1-4) — shared BM-2-901-13 masthead --
|
|
328
|
+
renderReportHeaderBand(wb, ws, `${summary.screen.toUpperCase()} TESTCASE`);
|
|
329
|
+
// Materialize rows 5..8 so the summary band + row-9 spacer land at the right offsets.
|
|
330
|
+
for (let r = 5; r <= 8; r++) ws.getRow(r);
|
|
230
331
|
|
|
231
332
|
// -- Row 5: spacer (height auto-fit). --
|
|
232
333
|
|
|
@@ -348,7 +449,12 @@ function addTestcaseSheet(
|
|
|
348
449
|
const emittedGroups = new Set<string>();
|
|
349
450
|
|
|
350
451
|
function emitGroup(groupName: string, groupRows: TestCaseRow[]): void {
|
|
351
|
-
|
|
452
|
+
// Column A (TC ID) must stay TRULY blank on a divider — never write '' here. COUNTA counts
|
|
453
|
+
// empty-string cells, so an '' in A made the generated total (=COUNTA(A15:A…)) over-count by
|
|
454
|
+
// one per group divider when recalculated (e.g. in Google Sheets). Set only the group name in B;
|
|
455
|
+
// the lavender band still spans every column via the fill loop below (style only, no value).
|
|
456
|
+
const divider = ws.addRow([]);
|
|
457
|
+
divider.getCell(2).value = groupName;
|
|
352
458
|
divider.getCell(2).font = { bold: true };
|
|
353
459
|
divider.eachCell({ includeEmpty: false }, (cell) => {
|
|
354
460
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: CATEGORY_FILL } };
|
|
@@ -37,6 +37,13 @@ export interface ParsedScenario {
|
|
|
37
37
|
stepsName?: string; // set when scenario has @steps:<name> — marks it as a reusable block
|
|
38
38
|
extendsName?: string; // set when scenario has @extend:<name> — merges base steps inline
|
|
39
39
|
hookType?: 'beforeAll' | 'afterEach' | 'afterAll'; // set when scenario has @beforeAll/@afterEach/@afterAll
|
|
40
|
+
/**
|
|
41
|
+
* Comment lines inside the scenario body (between its `Scenario:` line and the next
|
|
42
|
+
* scenario), leading `# ` stripped. `@cucumber/gherkin` keeps comments in a separate
|
|
43
|
+
* top-level array; we range-attach them here so the exporter can surface a @manual
|
|
44
|
+
* scenario's `# Tester verifies:` procedure (its only "steps") in the delivery report.
|
|
45
|
+
*/
|
|
46
|
+
comments?: string[];
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
export interface ParsedFeature {
|
|
@@ -82,17 +89,53 @@ export class GherkinParser {
|
|
|
82
89
|
const pathMatch = description.match(/^\s*Path:\s*(.+)$/m);
|
|
83
90
|
const path = pathMatch ? pathMatch[1].trim() : undefined;
|
|
84
91
|
|
|
92
|
+
const scenarios = this.parseScenarios(feature);
|
|
93
|
+
this.attachScenarioComments(gherkinDocument, feature, scenarios);
|
|
94
|
+
|
|
85
95
|
return {
|
|
86
96
|
name: feature.name,
|
|
87
97
|
description,
|
|
88
98
|
path,
|
|
89
99
|
tags,
|
|
90
100
|
background: this.parseBackground(feature),
|
|
91
|
-
scenarios
|
|
101
|
+
scenarios,
|
|
92
102
|
sourceFile: filePath,
|
|
93
103
|
};
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Range-attach top-level Gherkin comments to the scenario whose body contains them.
|
|
108
|
+
* A comment belongs to scenario i when its line is after scenario i's `Scenario:` line
|
|
109
|
+
* and before scenario i+1's header (its first tag line, or its `Scenario:` line). The
|
|
110
|
+
* leading `# ` is stripped. Used to surface a @manual scenario's tester-procedure comments
|
|
111
|
+
* (its only "steps") in the delivery report.
|
|
112
|
+
*/
|
|
113
|
+
private attachScenarioComments(
|
|
114
|
+
doc: Messages.GherkinDocument,
|
|
115
|
+
feature: Messages.Feature,
|
|
116
|
+
scenarios: ParsedScenario[]
|
|
117
|
+
): void {
|
|
118
|
+
const comments = doc.comments || [];
|
|
119
|
+
if (!comments.length) return;
|
|
120
|
+
const scenChildren = feature.children
|
|
121
|
+
.filter((c) => c.scenario)
|
|
122
|
+
.map((c) => c.scenario!);
|
|
123
|
+
// Same filter/order as parseScenarios → indices line up with `scenarios`.
|
|
124
|
+
for (let i = 0; i < scenChildren.length && i < scenarios.length; i++) {
|
|
125
|
+
const startLine = scenChildren[i].location.line;
|
|
126
|
+
const next = scenChildren[i + 1];
|
|
127
|
+
let endLine = Number.POSITIVE_INFINITY;
|
|
128
|
+
if (next) {
|
|
129
|
+
const tagLines = (next.tags || []).map((t) => t.location.line);
|
|
130
|
+
endLine = Math.min(next.location.line, ...(tagLines.length ? tagLines : [next.location.line]));
|
|
131
|
+
}
|
|
132
|
+
const body = comments
|
|
133
|
+
.filter((cm) => cm.location.line > startLine && cm.location.line < endLine)
|
|
134
|
+
.map((cm) => cm.text.replace(/^\s*#\s?/, '').replace(/\s+$/, ''));
|
|
135
|
+
if (body.length) scenarios[i].comments = body;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
96
139
|
/**
|
|
97
140
|
* Parse background steps
|
|
98
141
|
*/
|