@sun-asterisk/sungen 3.2.14 → 3.2.16-beta.1
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 +167 -51
- 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 +3 -46
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +1 -1
- package/dist/exporters/feature-parser.js +1 -1
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/locale-variants.d.ts +38 -0
- package/dist/exporters/locale-variants.d.ts.map +1 -0
- package/dist/exporters/locale-variants.js +159 -0
- package/dist/exporters/locale-variants.js.map +1 -0
- package/dist/exporters/matrix/build.d.ts +36 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -0
- package/dist/exporters/matrix/build.js +267 -0
- package/dist/exporters/matrix/build.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +47 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -0
- package/dist/exporters/matrix/export.js +136 -0
- package/dist/exporters/matrix/export.js.map +1 -0
- package/dist/exporters/matrix/fingerprint.d.ts +20 -0
- package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
- package/dist/exporters/matrix/fingerprint.js +83 -0
- package/dist/exporters/matrix/fingerprint.js.map +1 -0
- package/dist/exporters/matrix/gates.d.ts +21 -0
- package/dist/exporters/matrix/gates.d.ts.map +1 -0
- package/dist/exporters/matrix/gates.js +258 -0
- package/dist/exporters/matrix/gates.js.map +1 -0
- package/dist/exporters/matrix/map-loader.d.ts +22 -0
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
- package/dist/exporters/matrix/map-loader.js +173 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -0
- package/dist/exporters/matrix/render-csv.d.ts +8 -0
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
- package/dist/exporters/matrix/render-csv.js +69 -0
- package/dist/exporters/matrix/render-csv.js.map +1 -0
- package/dist/exporters/matrix/render-xlsx.d.ts +17 -0
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
- package/dist/exporters/matrix/render-xlsx.js +298 -0
- package/dist/exporters/matrix/render-xlsx.js.map +1 -0
- package/dist/exporters/matrix/types.d.ts +153 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -0
- package/dist/exporters/matrix/types.js +15 -0
- package/dist/exporters/matrix/types.js.map +1 -0
- package/dist/exporters/scenario-merger.js +1 -1
- 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/xlsx-report-builder.d.ts +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +3 -3
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
- package/dist/generators/test-generator/code-generator.js +1 -1
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +4 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +3 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +6 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +41 -2
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/harness/annotation-overrides.d.ts +4 -1
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +5 -2
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +6 -1
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability.d.ts +25 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +44 -2
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +5 -0
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +118 -120
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +43 -7
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
- package/dist/orchestrator/templates/specs-test-data.ts +13 -4
- package/package.json +4 -4
- package/src/cli/commands/delivery.ts +199 -71
- package/src/dashboard/snapshot-builder.ts +11 -61
- package/src/dashboard/templates/index.html +1 -1
- package/src/exporters/feature-parser.ts +1 -1
- package/src/exporters/locale-variants.ts +157 -0
- package/src/exporters/matrix/build.ts +294 -0
- package/src/exporters/matrix/export.ts +124 -0
- package/src/exporters/matrix/fingerprint.ts +49 -0
- package/src/exporters/matrix/gates.ts +284 -0
- package/src/exporters/matrix/map-loader.ts +142 -0
- package/src/exporters/matrix/render-csv.ts +71 -0
- package/src/exporters/matrix/render-xlsx.ts +289 -0
- package/src/exporters/matrix/types.ts +176 -0
- package/src/exporters/scenario-merger.ts +1 -1
- package/src/exporters/spec-parser.ts +1 -1
- package/src/exporters/xlsx-report-builder.ts +3 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
- package/src/generators/test-generator/code-generator.ts +1 -1
- package/src/generators/test-generator/diagnostics.ts +4 -1
- package/src/generators/test-generator/step-mapper.ts +43 -2
- package/src/harness/annotation-overrides.ts +5 -2
- package/src/harness/audit.ts +7 -2
- package/src/harness/capability.ts +53 -3
- package/src/harness/quality-gates.ts +4 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +1 -1
- package/src/orchestrator/templates/ai-src/commands/delivery.md +118 -120
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +43 -7
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
- package/src/orchestrator/templates/specs-test-data.ts +13 -4
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the Test Case & Coverage Matrix workbook.
|
|
3
|
+
*
|
|
4
|
+
* Sheets:
|
|
5
|
+
* 1. Testcases — one parent row per delivery item, variant sub-rows grouped with
|
|
6
|
+
* Excel outline (expanded for execution; the level-1 outline button collapses
|
|
7
|
+
* to the customer/review view — same sheet, one model, two views).
|
|
8
|
+
* 2. Coverage — target × category grid (items + variants, `—` marks the gaps),
|
|
9
|
+
* dispositions, and the generation manifest.
|
|
10
|
+
*
|
|
11
|
+
* Every sheet keeps the company ISO masthead (renderReportHeaderBand). A draft
|
|
12
|
+
* matrix (unapproved groups / review findings) carries a red DRAFT banner.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as ExcelJS from 'exceljs';
|
|
16
|
+
import {
|
|
17
|
+
renderReportHeaderBand,
|
|
18
|
+
allBordersBlack,
|
|
19
|
+
ARIAL_FONT,
|
|
20
|
+
BLACK,
|
|
21
|
+
LAVENDER,
|
|
22
|
+
} from '../xlsx-report-builder';
|
|
23
|
+
import { statusToTestResult, formatExecutedDate } from '../playwright-report-parser';
|
|
24
|
+
import { CoverageVariant, DeliveryItem, ItemResult, MatrixModel, MapCategory } from './types';
|
|
25
|
+
|
|
26
|
+
const CATEGORY_ORDER: MapCategory[] = ['normal', 'abnormal', 'security', 'nfr'];
|
|
27
|
+
const SOFT_BLUE = { argb: 'FFDCE6F1' };
|
|
28
|
+
const DRAFT_RED = { argb: 'FFC00000' };
|
|
29
|
+
const GAP_FILL = { argb: 'FFFCE4EC' };
|
|
30
|
+
|
|
31
|
+
function headerCell(c: ExcelJS.Cell, label: string): void {
|
|
32
|
+
c.value = label;
|
|
33
|
+
c.font = { bold: true, size: 10, color: BLACK, name: ARIAL_FONT };
|
|
34
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: LAVENDER };
|
|
35
|
+
c.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
36
|
+
c.border = allBordersBlack;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function dataCell(c: ExcelJS.Cell, value: ExcelJS.CellValue, opts?: { bold?: boolean; fill?: { argb: string }; center?: boolean; color?: { argb: string } }): void {
|
|
40
|
+
c.value = value;
|
|
41
|
+
c.font = { bold: opts?.bold ?? false, size: 10, name: ARIAL_FONT, ...(opts?.color ? { color: opts.color } : {}) };
|
|
42
|
+
c.alignment = { horizontal: opts?.center ? 'center' : 'left', vertical: 'top', wrapText: true };
|
|
43
|
+
c.border = allBordersBlack;
|
|
44
|
+
if (opts?.fill) c.fill = { type: 'pattern', pattern: 'solid', fgColor: opts.fill };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function itemResultLabel(item: DeliveryItem): string {
|
|
48
|
+
const { passed, failed, blocked, notRun } = item.resultCounts;
|
|
49
|
+
const total = item.variants.length;
|
|
50
|
+
const map: Record<ItemResult, string> = {
|
|
51
|
+
passed: `${passed}/${total} Passed`,
|
|
52
|
+
failed: `${passed}/${total} Passed · ${failed} Failed`,
|
|
53
|
+
blocked: `${blocked} Blocked`,
|
|
54
|
+
partial: `${passed}/${total} Passed · ${notRun} not run`,
|
|
55
|
+
not_run: 'Pending',
|
|
56
|
+
};
|
|
57
|
+
return map[item.result];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function variantResultLabel(v: CoverageVariant): string {
|
|
61
|
+
if (!v.result) return 'Pending';
|
|
62
|
+
return statusToTestResult(v.result.status);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function numbered(lines: string[]): string {
|
|
66
|
+
if (lines.length === 0) return '';
|
|
67
|
+
if (lines.length === 1) return lines[0];
|
|
68
|
+
return lines.map((l, i) => `${i + 1}. ${l}`).join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Red DRAFT banner on row 5 when the matrix is not approved for official delivery. */
|
|
72
|
+
function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
|
|
73
|
+
if (model.manifest.approvalState !== 'draft') return;
|
|
74
|
+
ws.mergeCells('B5:H5');
|
|
75
|
+
const c = ws.getCell('B5');
|
|
76
|
+
c.value = 'DRAFT — NOT APPROVED FOR OFFICIAL DELIVERY (review-required findings or proposed groups remain)';
|
|
77
|
+
c.font = { bold: true, size: 11, name: ARIAL_FONT, color: { argb: 'FFFFFFFF' } };
|
|
78
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: DRAFT_RED };
|
|
79
|
+
c.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Sheet 1 — Testcases (the matrix)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
const MATRIX_HEADERS = [
|
|
87
|
+
'ID', 'Target', 'Test Intent / Condition', 'Category', 'Priority', 'Precondition',
|
|
88
|
+
'Coverage / Test Data', 'Action / Trigger', 'Expected (Oracle)', 'Mode', 'Trace',
|
|
89
|
+
'Result', 'Executed Date', 'Executor', 'Note\n(Evidence, DefectID)',
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
|
|
93
|
+
const ws = wb.addWorksheet('Testcases');
|
|
94
|
+
ws.properties.outlineLevelRow = 1;
|
|
95
|
+
|
|
96
|
+
ws.columns = [
|
|
97
|
+
{ width: 3 }, // A margin
|
|
98
|
+
{ width: 20 }, // B ID
|
|
99
|
+
{ width: 18 }, // C Target
|
|
100
|
+
{ width: 42 }, // D Intent / Condition
|
|
101
|
+
{ width: 11 }, // E Category
|
|
102
|
+
{ width: 9 }, // F Priority
|
|
103
|
+
{ width: 26 }, // G Precondition
|
|
104
|
+
{ width: 34 }, // H Coverage / Data
|
|
105
|
+
{ width: 34 }, // I Trigger
|
|
106
|
+
{ width: 42 }, // J Expected
|
|
107
|
+
{ width: 11 }, // K Mode
|
|
108
|
+
{ width: 13 }, // L Trace
|
|
109
|
+
{ width: 17 }, // M Result
|
|
110
|
+
{ width: 13 }, // N Executed
|
|
111
|
+
{ width: 13 }, // O Executor
|
|
112
|
+
{ width: 24 }, // P Note
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
renderReportHeaderBand(wb, ws, `${model.unit.toUpperCase()} TEST CASE & COVERAGE MATRIX`, sungenVersion, model.formNo);
|
|
116
|
+
draftBanner(ws, model);
|
|
117
|
+
|
|
118
|
+
// Summary band (row 6) — items vs variants stay separate metrics by design (I8):
|
|
119
|
+
// the ITEM count is reviewer workload, the VARIANT count is execution workload.
|
|
120
|
+
const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
|
|
121
|
+
const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
|
|
122
|
+
const notRun = model.items.reduce((a, i) => a + i.resultCounts.notRun + i.resultCounts.blocked, 0);
|
|
123
|
+
const band = ws.getRow(6);
|
|
124
|
+
[
|
|
125
|
+
`Delivery items: ${model.manifest.itemCount}`,
|
|
126
|
+
`Coverage variants: ${model.manifest.variantCount}`,
|
|
127
|
+
`Passed: ${passed}`, `Failed: ${failed}`, `Pending: ${notRun}`,
|
|
128
|
+
].forEach((label, i) => {
|
|
129
|
+
const c = band.getCell(2 + i);
|
|
130
|
+
dataCell(c, label, { bold: true, fill: SOFT_BLUE, center: true });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const HEADER_ROW = 8;
|
|
134
|
+
const hr = ws.getRow(HEADER_ROW);
|
|
135
|
+
hr.height = 28;
|
|
136
|
+
MATRIX_HEADERS.forEach((label, i) => headerCell(hr.getCell(2 + i), label));
|
|
137
|
+
|
|
138
|
+
let rowIdx = HEADER_ROW + 1;
|
|
139
|
+
for (const item of model.items) {
|
|
140
|
+
// Parent row — the delivery item (the review unit).
|
|
141
|
+
const pr = ws.getRow(rowIdx++);
|
|
142
|
+
const single = item.variants.length === 1;
|
|
143
|
+
const only = single ? item.variants[0] : undefined;
|
|
144
|
+
const coverage = single
|
|
145
|
+
? numbered(only!.data)
|
|
146
|
+
: `${item.variants.length} variants: ${item.variants.map((v) => v.condition).join(' · ')}`;
|
|
147
|
+
const values: ExcelJS.CellValue[] = [
|
|
148
|
+
item.id,
|
|
149
|
+
item.target,
|
|
150
|
+
item.intent,
|
|
151
|
+
item.category,
|
|
152
|
+
item.priority,
|
|
153
|
+
numbered(item.precondition),
|
|
154
|
+
coverage,
|
|
155
|
+
numbered(item.trigger),
|
|
156
|
+
item.oracle,
|
|
157
|
+
item.mode === 'manual' ? `Manual${only?.manualReason ? ` (${only.manualReason})` : ''}` : 'Auto',
|
|
158
|
+
item.traces.join(', '),
|
|
159
|
+
itemResultLabel(item),
|
|
160
|
+
single ? formatExecutedDate(only!.result?.startTime) : '',
|
|
161
|
+
'',
|
|
162
|
+
item.review === 'proposed' ? 'REVIEW REQUIRED' : '',
|
|
163
|
+
];
|
|
164
|
+
values.forEach((v, i) => dataCell(pr.getCell(2 + i), v, {
|
|
165
|
+
bold: true,
|
|
166
|
+
fill: SOFT_BLUE,
|
|
167
|
+
center: i === 3 || i === 4 || i === 9,
|
|
168
|
+
...(String(values[14]).startsWith('REVIEW') && i === 14 ? { color: DRAFT_RED } : {}),
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
// Variant sub-rows — the execution units (outline level 1; collapse for review).
|
|
172
|
+
if (!single) {
|
|
173
|
+
for (const v of item.variants) {
|
|
174
|
+
const vr = ws.getRow(rowIdx++);
|
|
175
|
+
vr.outlineLevel = 1;
|
|
176
|
+
const vValues: ExcelJS.CellValue[] = [
|
|
177
|
+
v.ref,
|
|
178
|
+
'',
|
|
179
|
+
v.condition,
|
|
180
|
+
'', '', '',
|
|
181
|
+
numbered(v.data),
|
|
182
|
+
numbered(v.trigger),
|
|
183
|
+
numbered(v.oracle),
|
|
184
|
+
v.mode === 'manual' ? `Manual${v.manualReason ? ` (${v.manualReason})` : ''}` : 'Auto',
|
|
185
|
+
v.traces.join(', '),
|
|
186
|
+
variantResultLabel(v),
|
|
187
|
+
formatExecutedDate(v.result?.startTime),
|
|
188
|
+
'',
|
|
189
|
+
v.result?.error ? String(v.result.error).slice(0, 200) : '',
|
|
190
|
+
];
|
|
191
|
+
vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
ws.autoFilter = { from: { row: HEADER_ROW, column: 2 }, to: { row: rowIdx - 1, column: 16 } };
|
|
197
|
+
ws.views = [{ state: 'frozen', ySplit: HEADER_ROW }];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Sheet 2 — Coverage (overview grid + dispositions + manifest)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
function addCoverageSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
|
|
205
|
+
const ws = wb.addWorksheet('Coverage');
|
|
206
|
+
ws.columns = [
|
|
207
|
+
{ width: 3 }, { width: 30 }, { width: 16 }, { width: 16 }, { width: 16 }, { width: 16 },
|
|
208
|
+
{ width: 16 }, { width: 40 },
|
|
209
|
+
];
|
|
210
|
+
renderReportHeaderBand(wb, ws, `${model.unit.toUpperCase()} COVERAGE OVERVIEW`, sungenVersion, model.formNo);
|
|
211
|
+
draftBanner(ws, model);
|
|
212
|
+
|
|
213
|
+
// --- Target × category grid ---
|
|
214
|
+
let rowIdx = 7;
|
|
215
|
+
dataCell(ws.getRow(rowIdx).getCell(2), 'Coverage matrix — delivery items (coverage variants) per target × category', { bold: true });
|
|
216
|
+
rowIdx += 1;
|
|
217
|
+
const gridHeader = ws.getRow(rowIdx++);
|
|
218
|
+
headerCell(gridHeader.getCell(2), 'Target');
|
|
219
|
+
CATEGORY_ORDER.forEach((cat, i) => headerCell(gridHeader.getCell(3 + i), cat));
|
|
220
|
+
headerCell(gridHeader.getCell(3 + CATEGORY_ORDER.length), 'Total');
|
|
221
|
+
|
|
222
|
+
const targets = Array.from(new Set(model.items.map((i) => i.target)));
|
|
223
|
+
for (const target of targets) {
|
|
224
|
+
const row = ws.getRow(rowIdx++);
|
|
225
|
+
dataCell(row.getCell(2), target, { bold: true });
|
|
226
|
+
let totalItems = 0;
|
|
227
|
+
let totalVariants = 0;
|
|
228
|
+
CATEGORY_ORDER.forEach((cat, i) => {
|
|
229
|
+
const cell = row.getCell(3 + i);
|
|
230
|
+
const items = model.items.filter((it) => it.target === target && it.category === cat);
|
|
231
|
+
const variants = items.reduce((a, it) => a + it.variants.length, 0);
|
|
232
|
+
totalItems += items.length;
|
|
233
|
+
totalVariants += variants;
|
|
234
|
+
// `—` is an explicit statement: this target × category cell has NO coverage.
|
|
235
|
+
dataCell(cell, items.length === 0 ? '—' : `${items.length} (${variants})`, {
|
|
236
|
+
center: true,
|
|
237
|
+
...(items.length === 0 ? { fill: GAP_FILL } : {}),
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
dataCell(row.getCell(3 + CATEGORY_ORDER.length), `${totalItems} (${totalVariants})`, { center: true, bold: true });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// --- Dispositions ---
|
|
244
|
+
if (model.dispositions.length > 0) {
|
|
245
|
+
rowIdx += 1;
|
|
246
|
+
dataCell(ws.getRow(rowIdx).getCell(2), 'Not in the matrix (explicit dispositions)', { bold: true });
|
|
247
|
+
rowIdx += 1;
|
|
248
|
+
const dh = ws.getRow(rowIdx++);
|
|
249
|
+
['Source', 'Disposition', 'Reason', 'Title'].forEach((l, i) => headerCell(dh.getCell(2 + i), l));
|
|
250
|
+
for (const d of model.dispositions) {
|
|
251
|
+
const row = ws.getRow(rowIdx++);
|
|
252
|
+
dataCell(row.getCell(2), d.vpId);
|
|
253
|
+
dataCell(row.getCell(3), d.as, { center: true });
|
|
254
|
+
dataCell(row.getCell(4), d.reason);
|
|
255
|
+
dataCell(row.getCell(5), d.title);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- Manifest ---
|
|
260
|
+
rowIdx += 1;
|
|
261
|
+
dataCell(ws.getRow(rowIdx).getCell(2), 'Generation manifest', { bold: true });
|
|
262
|
+
rowIdx += 1;
|
|
263
|
+
const m = model.manifest;
|
|
264
|
+
const manifestRows: Array<[string, string]> = [
|
|
265
|
+
['Unit', m.unit],
|
|
266
|
+
['Approval state', m.approvalState],
|
|
267
|
+
['Delivery items', String(m.itemCount)],
|
|
268
|
+
['Coverage variants', String(m.variantCount)],
|
|
269
|
+
['Source fingerprint', m.sourceFingerprint],
|
|
270
|
+
['Map fingerprint', m.mapFingerprint],
|
|
271
|
+
['Transformer version', m.transformerVersion],
|
|
272
|
+
['Max variants / item (warn)', String(m.thresholds.maxVariantsPerItem)],
|
|
273
|
+
];
|
|
274
|
+
for (const [k, v] of manifestRows) {
|
|
275
|
+
const row = ws.getRow(rowIdx++);
|
|
276
|
+
dataCell(row.getCell(2), k, { bold: true, fill: SOFT_BLUE });
|
|
277
|
+
ws.mergeCells(rowIdx - 1, 3, rowIdx - 1, 7);
|
|
278
|
+
dataCell(row.getCell(3), v);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
export function renderMatrixXlsx(model: MatrixModel, sungenVersion?: string): ExcelJS.Workbook {
|
|
285
|
+
const wb = new ExcelJS.Workbook();
|
|
286
|
+
addMatrixSheet(wb, model, sungenVersion);
|
|
287
|
+
addCoverageSheet(wb, model, sungenVersion);
|
|
288
|
+
return wb;
|
|
289
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery v2 — Test Case & Coverage Matrix model (docs/spec/delivery-coverage-matrix-spec.md).
|
|
3
|
+
*
|
|
4
|
+
* Gherkin stays the source of test meaning; the matrix is a derived presentation:
|
|
5
|
+
* one delivery item = one target + one intent + one oracle family, containing one or
|
|
6
|
+
* more atomic coverage variants (scenario / @cases row). Grouping semantics live in the
|
|
7
|
+
* committed Delivery Map (`qa/<kind>/<unit>/delivery/<unit>.map.yaml`) — AI-proposed,
|
|
8
|
+
* gate-validated, QA-approved.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PlaywrightResult } from '../types';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Delivery Map (the committed grouping artifact)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export type MapCategory = 'normal' | 'abnormal' | 'security' | 'nfr';
|
|
18
|
+
export type ReviewState = 'proposed' | 'approved';
|
|
19
|
+
export type DispositionKind = 'excluded' | 'blocked' | 'covered_elsewhere' | 'accepted_risk';
|
|
20
|
+
|
|
21
|
+
export interface MapGroup {
|
|
22
|
+
/** Stable delivery id, unique per unit, order-independent (e.g. DI-EMAIL-FORMAT). */
|
|
23
|
+
id: string;
|
|
24
|
+
/** ONE primary target: field/component dot-path, function/flow phrase, or `METHOD /path`. */
|
|
25
|
+
target: string;
|
|
26
|
+
/** One coherent test intent (the primary behavior/rule verified). */
|
|
27
|
+
intent: string;
|
|
28
|
+
/** Oracle family in words — the shared Pass/Fail observation. */
|
|
29
|
+
oracle: string;
|
|
30
|
+
category: MapCategory;
|
|
31
|
+
review: ReviewState;
|
|
32
|
+
/** Variant refs: `VP-ID` (all @cases rows when the scenario has a dataset) or `VP-ID#label`. */
|
|
33
|
+
variants: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MapDisposition {
|
|
37
|
+
as: DispositionKind;
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DeliveryMap {
|
|
42
|
+
version: number;
|
|
43
|
+
unit: string;
|
|
44
|
+
/** ISO form number rendered in the workbook masthead (default BM-2-901-13). */
|
|
45
|
+
formNo?: string;
|
|
46
|
+
groups: MapGroup[];
|
|
47
|
+
/** VP-id → intentionally-not-grouped disposition. */
|
|
48
|
+
dispositions: Record<string, MapDisposition>;
|
|
49
|
+
/** VP-id → scenario fingerprint stamped at approval time (drift detector input). */
|
|
50
|
+
fingerprints: Record<string, string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Derived model (deterministic, computed from the .feature at build time)
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/** Execution layers a variant verifies on — cross-checked with the verification scope. */
|
|
58
|
+
export type MatrixLayer = 'ui' | 'api' | 'db';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One atomic coverage variant — the unit of execution, result, and traceability.
|
|
62
|
+
* Backed by one scenario, or one `@cases` dataset row of a scenario.
|
|
63
|
+
*/
|
|
64
|
+
export interface CoverageVariant {
|
|
65
|
+
/** `VP-ID` or `VP-ID#label` for a dataset row. */
|
|
66
|
+
ref: string;
|
|
67
|
+
vpId: string;
|
|
68
|
+
/** Dataset-row label when this variant is one `@cases` row. */
|
|
69
|
+
caseLabel?: string;
|
|
70
|
+
/** Human title (scenario name without the VP prefix; row label appended for @cases). */
|
|
71
|
+
title: string;
|
|
72
|
+
/** Viewpoint category from the VP prefix (LOGIC/VAL/UI/SEC/…). */
|
|
73
|
+
vpCategory: string;
|
|
74
|
+
priority: string; // High | Normal | Low
|
|
75
|
+
/** 'auto' | 'manual'; manual keeps its @manual:Mx reason. */
|
|
76
|
+
mode: 'auto' | 'manual';
|
|
77
|
+
manualReason?: string;
|
|
78
|
+
layers: MatrixLayer[];
|
|
79
|
+
/** @spec:* requirement traces. */
|
|
80
|
+
traces: string[];
|
|
81
|
+
/** Condition summary: differentiating dimension shown on the sub-row. */
|
|
82
|
+
condition: string;
|
|
83
|
+
/** Resolved data pairs (test-data values / dataset-row columns), `key: value` lines. */
|
|
84
|
+
data: string[];
|
|
85
|
+
/** Normalized trigger shape (When steps, `{{var}}` collapsed) — signature input. */
|
|
86
|
+
triggerShape: string[];
|
|
87
|
+
/** Trigger for display (When steps, vars resolved). */
|
|
88
|
+
trigger: string[];
|
|
89
|
+
/** Normalized oracle shape (Then steps / manual Observable+Oracle) — signature input. */
|
|
90
|
+
oracleShape: string[];
|
|
91
|
+
/** Oracle for display. */
|
|
92
|
+
oracle: string[];
|
|
93
|
+
/** Precondition profile: auth role + scenario-level Given shapes — signature input. */
|
|
94
|
+
preconditionProfile: string;
|
|
95
|
+
/** Human preconditions (auth + own Given steps). */
|
|
96
|
+
precondition: string[];
|
|
97
|
+
/** 'simple' (G*W*T*) | 'sequence' (mid-flow Given / multi round-trip) | 'manual'. */
|
|
98
|
+
procedureProfile: 'simple' | 'sequence' | 'manual';
|
|
99
|
+
/** Canonical scenario fingerprint (drift detection). */
|
|
100
|
+
fingerprint: string;
|
|
101
|
+
/** Execution result, when the run report has one for this variant. */
|
|
102
|
+
result?: PlaywrightResult;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type ItemResult = 'passed' | 'failed' | 'blocked' | 'partial' | 'not_run';
|
|
106
|
+
|
|
107
|
+
/** One summarized row in the matrix — a group of variants sharing the signature. */
|
|
108
|
+
export interface DeliveryItem {
|
|
109
|
+
id: string;
|
|
110
|
+
target: string;
|
|
111
|
+
intent: string;
|
|
112
|
+
oracle: string;
|
|
113
|
+
category: MapCategory;
|
|
114
|
+
review: ReviewState;
|
|
115
|
+
priority: string;
|
|
116
|
+
mode: 'auto' | 'manual';
|
|
117
|
+
layers: MatrixLayer[];
|
|
118
|
+
/** Union of variant traces (exact per-variant traces stay on the variants). */
|
|
119
|
+
traces: string[];
|
|
120
|
+
precondition: string[];
|
|
121
|
+
/** Common trigger when all variants share one shape, else '(per variant)'. */
|
|
122
|
+
trigger: string[];
|
|
123
|
+
variants: CoverageVariant[];
|
|
124
|
+
/** Derived roll-up — never entered independently (Gate F). */
|
|
125
|
+
result: ItemResult;
|
|
126
|
+
resultCounts: { passed: number; failed: number; blocked: number; notRun: number };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface MatrixDisposition {
|
|
130
|
+
vpId: string;
|
|
131
|
+
title: string;
|
|
132
|
+
as: DispositionKind;
|
|
133
|
+
reason: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Gate findings
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
export type GateId = 'A' | 'B' | 'C' | 'D' | 'E' | 'G';
|
|
141
|
+
export type FindingSeverity = 'error' | 'review' | 'warning';
|
|
142
|
+
|
|
143
|
+
export interface MatrixFinding {
|
|
144
|
+
gate: GateId;
|
|
145
|
+
severity: FindingSeverity;
|
|
146
|
+
/** Delivery item / VP-id the finding anchors to, when applicable. */
|
|
147
|
+
ref?: string;
|
|
148
|
+
message: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// The assembled matrix
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
export interface MatrixManifest {
|
|
156
|
+
unit: string;
|
|
157
|
+
transformerVersion: string;
|
|
158
|
+
sourceFingerprint: string; // canonical hash over all scenario fingerprints
|
|
159
|
+
mapFingerprint: string; // canonical hash of the map content
|
|
160
|
+
approvalState: 'approved' | 'draft';
|
|
161
|
+
itemCount: number;
|
|
162
|
+
variantCount: number;
|
|
163
|
+
thresholds: { maxVariantsPerItem: number };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface MatrixModel {
|
|
167
|
+
unit: string;
|
|
168
|
+
formNo: string;
|
|
169
|
+
items: DeliveryItem[];
|
|
170
|
+
dispositions: MatrixDisposition[];
|
|
171
|
+
findings: MatrixFinding[];
|
|
172
|
+
manifest: MatrixManifest;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Complexity warning threshold (rules draft Gate I) — echoed in the manifest. */
|
|
176
|
+
export const MAX_VARIANTS_PER_ITEM = 12;
|
|
@@ -162,7 +162,7 @@ function findMatchingSpecTest(scenario: ScenarioMetadata, tests: SpecTest[]): Sp
|
|
|
162
162
|
if (exact) return exact;
|
|
163
163
|
|
|
164
164
|
// Fallback: match by VP-ID prefix (ignore trailing text differences)
|
|
165
|
-
const vpMatch = scenario.name.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)/);
|
|
165
|
+
const vpMatch = scenario.name.match(/^(VP-[A-Z][A-Z0-9]*-\d+(?:[a-zA-Z]|-[A-Z0-9]+)?)/);
|
|
166
166
|
if (vpMatch) {
|
|
167
167
|
const vpId = vpMatch[1];
|
|
168
168
|
const byVp = tests.find((t) => t.vpId === vpId);
|
|
@@ -165,7 +165,7 @@ function classifyBlock(block: CodeBlock): 'precondition' | 'step' | 'expectation
|
|
|
165
165
|
* Try to extract VP ID (e.g., VP-UI-001) from the start of a scenario title.
|
|
166
166
|
*/
|
|
167
167
|
function extractVpId(title: string): string | undefined {
|
|
168
|
-
const match = title.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)\b/);
|
|
168
|
+
const match = title.match(/^(VP-[A-Z][A-Z0-9]*-\d+(?:[a-zA-Z]|-[A-Z0-9]+)?)\b/);
|
|
169
169
|
return match ? match[1] : undefined;
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -143,6 +143,7 @@ export function renderReportHeaderBand(
|
|
|
143
143
|
ws: ExcelJS.Worksheet,
|
|
144
144
|
titleLabel: string,
|
|
145
145
|
sungenVersion?: string,
|
|
146
|
+
formNo: string = 'BM-2-901-13',
|
|
146
147
|
): void {
|
|
147
148
|
const issueDate = (() => {
|
|
148
149
|
const d = new Date();
|
|
@@ -187,9 +188,9 @@ export function renderReportHeaderBand(
|
|
|
187
188
|
d1.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
188
189
|
d1.border = allBordersBlack;
|
|
189
190
|
|
|
190
|
-
// G1 — No:
|
|
191
|
+
// G1 — No: <form number> (the company ISO form id; configurable via the delivery map)
|
|
191
192
|
const g1 = ws.getCell('G1');
|
|
192
|
-
g1.value =
|
|
193
|
+
g1.value = `No: ${formNo}`;
|
|
193
194
|
g1.font = { size: 12, name: TIMES };
|
|
194
195
|
g1.alignment = { vertical: 'middle' };
|
|
195
196
|
g1.border = { top: thinBlack, left: thinBlack, right: thinBlack };
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
{{~#if isGiven~}}
|
|
2
1
|
{
|
|
3
|
-
const
|
|
2
|
+
const table = {{> locator}};
|
|
3
|
+
const rows = table.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true });
|
|
4
4
|
{{#each assertions}}
|
|
5
|
-
{{this}}
|
|
5
|
+
{{{this}}}
|
|
6
6
|
{{/each~}}
|
|
7
7
|
}
|
|
8
|
-
{{~else~}}
|
|
9
|
-
{
|
|
10
|
-
const rows = {{> locator}}.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true });
|
|
11
|
-
{{#each assertions}}
|
|
12
|
-
{{this}}
|
|
13
|
-
{{/each~}}
|
|
14
|
-
}
|
|
15
|
-
{{~/if}}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
{{~#if rowSelector}}
|
|
2
|
+
{{~#if isGiven}}
|
|
3
|
+
const tableRow = {{> locator}}.locator('{{rowSelector}}');
|
|
4
|
+
await tableRow.filter({ hasText: '{{escapeQuotes filterValue}}' }).waitFor();
|
|
5
|
+
{{~else}}
|
|
6
|
+
const tableRow = {{> locator}}.locator('{{rowSelector}}');
|
|
7
|
+
await expect(tableRow).toContainText('{{escapeQuotes filterValue}}');
|
|
8
|
+
{{~/if}}
|
|
9
|
+
{{~else}}
|
|
1
10
|
{{~#if isGiven}}
|
|
2
11
|
const tableRow = {{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
|
|
3
12
|
await tableRow.waitFor();
|
|
4
13
|
{{~else}}
|
|
5
14
|
const tableRow = {{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
|
|
6
15
|
await expect(tableRow).toBeVisible();
|
|
7
|
-
{{~/if}}
|
|
16
|
+
{{~/if}}
|
|
17
|
+
{{~/if}}
|
|
@@ -908,7 +908,7 @@ export class CodeGenerator {
|
|
|
908
908
|
// them. Rewrite those to the per-row `rowData` view — each row then fires its own `@api` call
|
|
909
909
|
// with that row's input and asserts that row's expected status/body (the success/failure matrix).
|
|
910
910
|
// `testData.cases()/withRow()` (the global loader, no `.get/.bind/.set`) is intentionally left alone.
|
|
911
|
-
return transformToRuntimeData(rendered, 'rowData').replace(/\btestData\.(get|bind|set)\(/g, 'rowData.$1(');
|
|
911
|
+
return transformToRuntimeData(rendered, 'rowData').replace(/\btestData\.(get|bind|set|raw)\(/g, 'rowData.$1(');
|
|
912
912
|
}
|
|
913
913
|
return rendered;
|
|
914
914
|
}
|
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
* action line + ≥1 labeled Observable:/Oracle:/Expected: line
|
|
18
18
|
* SG-W009 — test-data references a `${QA_*}` secret that is declared in neither .env.qa
|
|
19
19
|
* nor .env.qa.example — the test will throw "not set" the moment the key is used
|
|
20
|
+
* SG-W010 — a row step's [Ref] looks POSITIONAL (First/Last/Top/…) but has no selectors.yaml
|
|
21
|
+
* entry: the label is display-only, so the step checks existence anywhere in the
|
|
22
|
+
* table — an ordering rule would pass even when the row moved
|
|
20
23
|
*
|
|
21
24
|
* Diagnostics never block generation by themselves; `sungen generate --strict`
|
|
22
25
|
* turns any collected diagnostic into a non-zero exit.
|
|
23
26
|
*/
|
|
24
27
|
|
|
25
|
-
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009';
|
|
28
|
+
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010';
|
|
26
29
|
|
|
27
30
|
export interface Diagnostic {
|
|
28
31
|
code: DiagnosticCode;
|
|
@@ -37,6 +37,10 @@ export class StepMapper {
|
|
|
37
37
|
/** Row scope: when active, column assertions are scoped to this row */
|
|
38
38
|
private inRowScope: boolean = false;
|
|
39
39
|
private rowScopeTable: string = '';
|
|
40
|
+
/** Count of row-scope-entering steps in the current scenario + whether the shared
|
|
41
|
+
* `tableRow` binding has been emitted (let-once + reassign when there are ≥2). */
|
|
42
|
+
private tableRowScopeSteps: number = 0;
|
|
43
|
+
private tableRowDeclared: boolean = false;
|
|
40
44
|
/** SG-W007: an action step already ran in this scenario (alert handlers registered after it are too late) */
|
|
41
45
|
private sawActionStep: boolean = false;
|
|
42
46
|
/** Compile-time warning channel (SG-W001/W002); optional — absent means silent legacy behavior */
|
|
@@ -109,6 +113,12 @@ export class StepMapper {
|
|
|
109
113
|
/**
|
|
110
114
|
* Set scenario context for path variable resolution
|
|
111
115
|
*/
|
|
116
|
+
/** A `see [Ref] row in [Table] table with {{v}}` step — declares `tableRow` and enters row scope. */
|
|
117
|
+
private isRowScopeEntryStep(step: ParsedStep): boolean {
|
|
118
|
+
return step.elementType === 'row' && /\bin\b.*\btable\b/i.test(step.text) &&
|
|
119
|
+
/\bwith\b/i.test(step.text) && !/\bis\s+hidden\b/i.test(step.text);
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
setScenarioContext(steps: ParsedStep[]): void {
|
|
113
123
|
this.currentScenarioSteps = steps;
|
|
114
124
|
// Reset dialog and frame scope at the start of each new scenario
|
|
@@ -116,6 +126,16 @@ export class StepMapper {
|
|
|
116
126
|
this.inFrameScope = false;
|
|
117
127
|
this.inRowScope = false;
|
|
118
128
|
this.rowScopeTable = '';
|
|
129
|
+
// Row-scope steps declare `const tableRow` at FUNCTION scope on purpose — the
|
|
130
|
+
// following `[Col] column with {{v}}` steps reference it, so the declaration
|
|
131
|
+
// can't be block-wrapped like the self-contained row ACTIONS are. But two or
|
|
132
|
+
// more row steps in one scenario then redeclare the same const → the whole
|
|
133
|
+
// spec fails to parse ("Identifier 'tableRow' has already been declared").
|
|
134
|
+
// Count them up front: with ≥2 the first becomes `let` and the rest plain
|
|
135
|
+
// reassignments (which also correctly REBINDS the row scope); a single
|
|
136
|
+
// occurrence keeps `const` so existing single-row specs stay byte-identical.
|
|
137
|
+
this.tableRowScopeSteps = steps.filter((s) => this.isRowScopeEntryStep(s)).length;
|
|
138
|
+
this.tableRowDeclared = false;
|
|
119
139
|
this.sawActionStep = false;
|
|
120
140
|
this.lastPrimaryKeyword = 'Given';
|
|
121
141
|
this.dataResolver.clearCaptured(); // captured vars are scenario-scoped
|
|
@@ -245,13 +265,34 @@ export class StepMapper {
|
|
|
245
265
|
|
|
246
266
|
if (mappedStep) {
|
|
247
267
|
// Enter row scope when a row-in-table step is matched
|
|
248
|
-
if (
|
|
249
|
-
/\bwith\b/i.test(step.text) && !/\bis\s+hidden\b/i.test(step.text)) {
|
|
268
|
+
if (this.isRowScopeEntryStep(step)) {
|
|
250
269
|
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
251
270
|
this.inRowScope = true;
|
|
252
271
|
this.rowScopeTable = brackets.length >= 2
|
|
253
272
|
? brackets[1].replace(/[\[\]]/g, '')
|
|
254
273
|
: '';
|
|
274
|
+
// ≥2 row steps in this scenario: turn the duplicate `const tableRow`
|
|
275
|
+
// declarations into one `let` + reassignments (see setScenarioContext).
|
|
276
|
+
if (this.tableRowScopeSteps > 1 && mappedStep.code && mappedStep.code.includes('const tableRow')) {
|
|
277
|
+
if (this.tableRowDeclared) {
|
|
278
|
+
mappedStep.code = mappedStep.code.replace('const tableRow', 'tableRow');
|
|
279
|
+
} else {
|
|
280
|
+
mappedStep.code = mappedStep.code.replace('const tableRow', 'let tableRow');
|
|
281
|
+
this.tableRowDeclared = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// SG-W010 — a POSITIONAL-looking row label ([First X Row], [Last …]) without a
|
|
285
|
+
// selectors.yaml entry is display-only: the step degrades to an anywhere-in-the-
|
|
286
|
+
// table existence check and an ordering rule silently never fails. A resolved
|
|
287
|
+
// entry compiles to a positioned toContainText — its absence is detectable here.
|
|
288
|
+
const rowLabel = (brackets[0] || '').replace(/[\[\]]/g, '');
|
|
289
|
+
if (this.diagnostics && rowLabel &&
|
|
290
|
+
/\b(first|last|top|bottom|nth|\d+(st|nd|rd|th)?)\b/i.test(rowLabel) &&
|
|
291
|
+
mappedStep.code && !mappedStep.code.includes('toContainText')) {
|
|
292
|
+
this.diagnostics.add('SG-W010',
|
|
293
|
+
`row label "[${rowLabel}]" looks positional but has no selectors.yaml entry — it is a display label only, so this step checks existence ANYWHERE in the table (an ordering rule would pass even when the row moved). Declare it to position the row, e.g. "${rowLabel.toLowerCase()}:\\n type: locator\\n value: 'tbody tr:first-child'"`,
|
|
294
|
+
{ feature: this.featureName, step: step.text });
|
|
295
|
+
}
|
|
255
296
|
}
|
|
256
297
|
|
|
257
298
|
if (this.verbose) {
|