@sun-asterisk/sungen 2.6.8 → 2.6.11
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/dashboard.d.ts +2 -1
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +9 -9
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +33 -0
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/history-store.d.ts +13 -9
- package/dist/dashboard/history-store.d.ts.map +1 -1
- package/dist/dashboard/history-store.js +19 -28
- package/dist/dashboard/history-store.js.map +1 -1
- package/dist/dashboard/html-renderer.d.ts +1 -1
- package/dist/dashboard/html-renderer.d.ts.map +1 -1
- package/dist/dashboard/html-renderer.js +2 -2
- package/dist/dashboard/html-renderer.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +38 -2
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +153 -221
- package/dist/exporters/csv-exporter.d.ts +4 -0
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +35 -26
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +16 -4
- 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 +28 -20
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +22 -5
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/scenario-merger.d.ts +23 -1
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +39 -0
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/step-formatter.d.ts +31 -3
- package/dist/exporters/step-formatter.d.ts.map +1 -1
- package/dist/exporters/step-formatter.js +52 -19
- package/dist/exporters/step-formatter.js.map +1 -1
- package/dist/exporters/sun-logo.d.ts +10 -0
- package/dist/exporters/sun-logo.d.ts.map +1 -0
- package/dist/exporters/sun-logo.js +13 -0
- package/dist/exporters/sun-logo.js.map +1 -0
- package/dist/exporters/test-data-resolver.d.ts +13 -5
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +36 -14
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +16 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +6 -0
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +204 -100
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +4 -3
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
- package/dist/orchestrator/templates/playwright.config.js +2 -0
- package/dist/orchestrator/templates/playwright.config.js.map +1 -1
- package/dist/orchestrator/templates/playwright.config.ts +2 -0
- package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-base.js +1 -5
- package/dist/orchestrator/templates/specs-base.js.map +1 -1
- package/dist/orchestrator/templates/specs-base.ts +1 -5
- package/package.json +1 -1
- package/src/cli/commands/dashboard.ts +9 -9
- package/src/cli/commands/delivery.ts +30 -0
- package/src/cli/index.ts +1 -1
- package/src/dashboard/history-store.ts +22 -28
- package/src/dashboard/html-renderer.ts +6 -2
- package/src/dashboard/snapshot-builder.ts +36 -2
- package/src/dashboard/templates/index.html +153 -221
- package/src/dashboard/types.ts +1 -1
- package/src/exporters/csv-exporter.ts +44 -27
- package/src/exporters/feature-parser.ts +27 -8
- package/src/exporters/json-exporter.ts +31 -21
- package/src/exporters/playwright-report-parser.ts +23 -5
- package/src/exporters/scenario-merger.ts +65 -1
- package/src/exporters/step-formatter.ts +48 -23
- package/src/exporters/sun-logo.ts +10 -0
- package/src/exporters/test-data-resolver.ts +37 -13
- package/src/exporters/types.ts +18 -1
- package/src/exporters/xlsx-exporter.ts +216 -102
- package/src/orchestrator/project-initializer.ts +4 -3
- package/src/orchestrator/templates/playwright.config.ts +2 -0
- package/src/orchestrator/templates/specs-base.ts +1 -5
|
@@ -8,7 +8,11 @@ import { parse as parseYaml } from 'yaml';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Load test-data.yaml into a flat key→value map.
|
|
11
|
-
*
|
|
11
|
+
* Nested objects are flattened with dot notation so flow files like
|
|
12
|
+
* kudos:
|
|
13
|
+
* recipient: "An"
|
|
14
|
+
* become { "kudos.recipient": "An" }, matching {{kudos.recipient}} refs.
|
|
15
|
+
* Arrays are JSON-stringified. Returns empty map if file missing.
|
|
12
16
|
*/
|
|
13
17
|
export function loadTestData(testDataFilePath: string): Record<string, string> {
|
|
14
18
|
if (!fs.existsSync(testDataFilePath)) return {};
|
|
@@ -16,38 +20,58 @@ export function loadTestData(testDataFilePath: string): Record<string, string> {
|
|
|
16
20
|
const parsed = parseYaml(content);
|
|
17
21
|
if (!parsed || typeof parsed !== 'object') return {};
|
|
18
22
|
|
|
19
|
-
// Flatten shallow object to string values
|
|
20
23
|
const result: Record<string, string> = {};
|
|
21
|
-
|
|
24
|
+
flatten(parsed as Record<string, unknown>, '', result);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function flatten(obj: Record<string, unknown>, prefix: string, out: Record<string, string>): void {
|
|
29
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
22
30
|
if (value === null || value === undefined) continue;
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
out[path] = JSON.stringify(value);
|
|
34
|
+
} else if (typeof value === 'object') {
|
|
35
|
+
flatten(value as Record<string, unknown>, path, out);
|
|
36
|
+
} else {
|
|
37
|
+
out[path] = String(value);
|
|
38
|
+
}
|
|
25
39
|
}
|
|
26
|
-
return result;
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
/**
|
|
30
|
-
* Format a list of variable references into a "key: value
|
|
31
|
-
* suitable for the CSV Test Data column.
|
|
43
|
+
* Format a list of variable references into a "key: value" string.
|
|
32
44
|
*
|
|
33
|
-
*
|
|
45
|
+
* - `separator` controls how pairs are joined. Default `'; '` keeps CSV/XLSX
|
|
46
|
+
* cells on one line. Pass `'\n'` for surfaces that render multi-line text
|
|
47
|
+
* (e.g. the dashboard modal uses whitespace-pre-wrap).
|
|
48
|
+
* - Test data is never truncated by default — values represent real test
|
|
49
|
+
* inputs and clients need to see them in full. Pass a finite `maxLen` only
|
|
50
|
+
* if the downstream surface requires a cap (e.g. terminal preview).
|
|
34
51
|
*/
|
|
35
52
|
export function formatTestData(
|
|
36
53
|
referencedVars: string[],
|
|
37
|
-
testData: Record<string, string
|
|
54
|
+
testData: Record<string, string>,
|
|
55
|
+
maxLen: number = Infinity,
|
|
56
|
+
separator: string = '; ',
|
|
38
57
|
): string {
|
|
39
58
|
if (referencedVars.length === 0) return '';
|
|
59
|
+
// When joining with newlines (dashboard multi-line view), prefix each line
|
|
60
|
+
// with a bullet point so the list scans easily. CSV-style "; " join skips
|
|
61
|
+
// the bullet — single-cell text shouldn't have list markers.
|
|
62
|
+
const useBullet = separator.includes('\n');
|
|
40
63
|
const pairs: string[] = [];
|
|
41
64
|
for (const key of referencedVars) {
|
|
42
65
|
const value = testData[key];
|
|
43
66
|
if (value === undefined) continue;
|
|
44
|
-
|
|
67
|
+
const line = `${key}: ${truncate(value, maxLen)}`;
|
|
68
|
+
pairs.push(useBullet ? `• ${line}` : line);
|
|
45
69
|
}
|
|
46
|
-
return pairs.join(
|
|
70
|
+
return pairs.join(separator);
|
|
47
71
|
}
|
|
48
72
|
|
|
49
73
|
function truncate(s: string, max: number): string {
|
|
50
|
-
if (s.length <= max) return s;
|
|
74
|
+
if (!isFinite(max) || s.length <= max) return s;
|
|
51
75
|
return s.substring(0, max - 1) + '…';
|
|
52
76
|
}
|
|
53
77
|
|
package/src/exporters/types.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface TestCaseRow {
|
|
|
15
15
|
testData: string; // semicolon-separated key: value
|
|
16
16
|
steps: string; // numbered steps
|
|
17
17
|
expectedResults: string; // numbered expected results
|
|
18
|
-
priority: string; //
|
|
18
|
+
priority: string; // High | Normal | Low
|
|
19
19
|
testcaseType: string; // Auto | Manual | Not compiled
|
|
20
20
|
testResult: string; // Passed | Failed | Pending | N/A
|
|
21
21
|
executedDate: string; // dd/mm/yyyy
|
|
@@ -59,9 +59,20 @@ export interface FeatureMetadata {
|
|
|
59
59
|
featureName: string; // e.g., "Create Kudo Modal"
|
|
60
60
|
featurePath?: string; // e.g., "/kudos"
|
|
61
61
|
featureTags: string[]; // Feature-level tags
|
|
62
|
+
/** Background block classified by bucket. Shared across all runnable
|
|
63
|
+
* scenarios — the merger is responsible for prepending these. */
|
|
64
|
+
backgroundGivenSteps: string[];
|
|
65
|
+
backgroundWhenSteps: string[];
|
|
66
|
+
backgroundThenSteps: string[];
|
|
67
|
+
backgroundOrderedSteps: OrderedStep[];
|
|
62
68
|
scenarios: ScenarioMetadata[];
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
export interface OrderedStep {
|
|
72
|
+
text: string;
|
|
73
|
+
bucket: 'given' | 'when' | 'then';
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
export interface ScenarioMetadata {
|
|
66
77
|
name: string; // Full scenario name (with VP prefix)
|
|
67
78
|
tags: string[]; // Scenario-level tags (merged with feature tags)
|
|
@@ -71,6 +82,12 @@ export interface ScenarioMetadata {
|
|
|
71
82
|
rawGivenSteps: string[]; // Raw Given/And-after-Given step text
|
|
72
83
|
rawWhenSteps: string[]; // Raw When/And-after-When step text
|
|
73
84
|
rawThenSteps: string[]; // Raw Then/And-after-Then step text
|
|
85
|
+
/**
|
|
86
|
+
* Steps in original chronological order with each step labelled by its
|
|
87
|
+
* effective bucket (And/But inherits from the prior explicit keyword).
|
|
88
|
+
* Use this when ordering matters — e.g. mid-scenario `Given` after `When`.
|
|
89
|
+
*/
|
|
90
|
+
orderedSteps: OrderedStep[];
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
/**
|
|
@@ -12,14 +12,16 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import ExcelJS from 'exceljs';
|
|
15
|
+
import JSZip from 'jszip';
|
|
15
16
|
import { ScreenSummary, TestCaseRow } from './types';
|
|
16
17
|
import { getPackageVersion } from './package-info';
|
|
18
|
+
import { SUN_LOGO_PNG_BASE64 } from './sun-logo';
|
|
17
19
|
|
|
18
20
|
const COL_COUNT = 16;
|
|
19
|
-
const HEADER_FILL = '
|
|
20
|
-
const HEADER_FONT = '
|
|
21
|
+
const HEADER_FILL = 'FFD9D2E9'; // lavender — matches the summary-header band on row 6
|
|
22
|
+
const HEADER_FONT = 'FF000000'; // black text reads better on the light lavender
|
|
21
23
|
const META_FILL = 'FFDCE6F1'; // soft blue for meta rows
|
|
22
|
-
const CATEGORY_FILL = '
|
|
24
|
+
const CATEGORY_FILL = 'FFD9D2E9'; // same lavender for group dividers (Accessing / GUI / Function)
|
|
23
25
|
const BORDER_COLOR = 'FFBFBFBF';
|
|
24
26
|
|
|
25
27
|
type AnyCell = ExcelJS.Cell;
|
|
@@ -51,111 +53,180 @@ export function renderXlsx(
|
|
|
51
53
|
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
|
|
52
54
|
})();
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
const pct = (n: number) => `${Math.round((n / total) * 100)}%`;
|
|
56
|
-
|
|
57
|
-
// -- Column widths (matches CSV column order) --
|
|
56
|
+
// -- Column widths matching template_report.xlsx (Sample sheet) --
|
|
58
57
|
ws.columns = [
|
|
59
|
-
{ width:
|
|
60
|
-
{ width:
|
|
61
|
-
{ width:
|
|
62
|
-
{ width:
|
|
63
|
-
{ width:
|
|
64
|
-
{ width:
|
|
65
|
-
{ width:
|
|
66
|
-
{ width:
|
|
67
|
-
{ width:
|
|
68
|
-
{ width:
|
|
69
|
-
{ width:
|
|
70
|
-
{ width:
|
|
71
|
-
{ width:
|
|
72
|
-
{ width: 16 },
|
|
73
|
-
{ width:
|
|
74
|
-
{ width:
|
|
58
|
+
{ width: 20 }, // A — TC ID (wider to fit long flow IDs like FLOW-KUDO-…)
|
|
59
|
+
{ width: 12.5 }, // B — Screen/Function
|
|
60
|
+
{ width: 15.38 }, // C — Big item
|
|
61
|
+
{ width: 16.25 }, // D — Medium item
|
|
62
|
+
{ width: 17 }, // E — Test Object
|
|
63
|
+
{ width: 25.5 }, // F — Pre-condition
|
|
64
|
+
{ width: 24.88 }, // G — Test Data
|
|
65
|
+
{ width: 24.88 }, // H — Steps
|
|
66
|
+
{ width: 41 }, // I — Expected results
|
|
67
|
+
{ width: 12 }, // J — Priority
|
|
68
|
+
{ width: 13.5 }, // K — Testcase type
|
|
69
|
+
{ width: 15.88 }, // L — Test Result
|
|
70
|
+
{ width: 13.5 }, // M — Executed Date
|
|
71
|
+
{ width: 16.5 }, // N — Test Executor
|
|
72
|
+
{ width: 23 }, // O — Test Environment
|
|
73
|
+
{ width: 29 }, // P — Note
|
|
75
74
|
];
|
|
76
75
|
|
|
77
|
-
// -- Top metadata band (rows 1-4) --
|
|
76
|
+
// -- Top metadata band (rows 1-4) — 1:1 with template_report.xlsx --
|
|
77
|
+
const TIMES = 'Times New Roman';
|
|
78
|
+
const ARIAL_FONT = 'Arial';
|
|
79
|
+
const BLACK = { argb: 'FF000000' };
|
|
80
|
+
const WHITE = { argb: 'FFFFFFFF' };
|
|
81
|
+
const LAVENDER = { argb: 'FFD9D2E9' };
|
|
82
|
+
const thinBlack = { style: 'thin' as const, color: BLACK };
|
|
83
|
+
const allBordersBlack = { top: thinBlack, left: thinBlack, bottom: thinBlack, right: thinBlack };
|
|
84
|
+
|
|
85
|
+
// Ensure rows 1..8 exist before merging.
|
|
86
|
+
for (let r = 1; r <= 8; r++) ws.getRow(r);
|
|
87
|
+
|
|
88
|
+
ws.mergeCells('A1:C3');
|
|
89
|
+
ws.mergeCells('D1:F3');
|
|
90
|
+
ws.mergeCells('G1:H1');
|
|
91
|
+
ws.mergeCells('G2:H2');
|
|
92
|
+
ws.mergeCells('G3:H3');
|
|
93
|
+
ws.mergeCells('A4:F4');
|
|
94
|
+
ws.mergeCells('G4:H4');
|
|
95
|
+
|
|
96
|
+
// A1 (logo band, merged A1:C3) — border + embedded Sun* logo (base64 inline).
|
|
97
|
+
const a1 = ws.getCell('A1');
|
|
98
|
+
a1.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
99
|
+
a1.border = allBordersBlack;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const imageId = wb.addImage({
|
|
103
|
+
buffer: Buffer.from(SUN_LOGO_PNG_BASE64, 'base64') as unknown as ExcelJS.Buffer,
|
|
104
|
+
extension: 'png',
|
|
105
|
+
});
|
|
106
|
+
// Centre a fixed-size 90×51 logo inside A1:C3 using absolute EMU offsets.
|
|
107
|
+
// Col widths (A=20, B=12.5, C=15.38) → pixels via Excel's `w*7+5` rule:
|
|
108
|
+
// A=145, B=92.5, C=112.66 ⇒ A1:C3 ≈ 350 px wide.
|
|
109
|
+
// Left padding for a 90-px image = (350−90)/2 ≈ 130 px = 1,237,500 EMU.
|
|
110
|
+
// 3 default rows × 20 px = 60 px; top padding ≈ 4.5 px ≈ 42,862 EMU.
|
|
111
|
+
ws.addImage(imageId, {
|
|
112
|
+
tl: { nativeCol: 0, nativeColOff: 1237500, nativeRow: 0, nativeRowOff: 42862 } as unknown as ExcelJS.Anchor,
|
|
113
|
+
ext: { width: 90, height: 51 },
|
|
114
|
+
editAs: 'oneCell',
|
|
115
|
+
} as unknown as ExcelJS.ImageRange);
|
|
116
|
+
} catch { /* logo is decorative — never block export */ }
|
|
117
|
+
|
|
118
|
+
// D1 (TESTCASE title)
|
|
78
119
|
const titleLabel = `${summary.screen.toUpperCase()} TESTCASE`;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
titleRow.height = 22;
|
|
85
|
-
|
|
86
|
-
const versionRow = ws.addRow(['', '', '', '', '', '', `Version: ${getPackageVersion()}`]);
|
|
87
|
-
versionRow.getCell(7).font = { bold: true };
|
|
88
|
-
const dateRow = ws.addRow(['', '', '', '', '', '', `Issue Date: ${issueDate}`]);
|
|
89
|
-
dateRow.getCell(7).font = { bold: true };
|
|
90
|
-
const companyRow = ws.addRow([
|
|
91
|
-
'SUN ASTERISK VIETNAM CO., LTD',
|
|
92
|
-
'',
|
|
93
|
-
'',
|
|
94
|
-
'',
|
|
95
|
-
'',
|
|
96
|
-
'',
|
|
97
|
-
'ISO/IEC 27001:2022 & ISO 9001:2015',
|
|
98
|
-
]);
|
|
99
|
-
companyRow.getCell(1).font = { bold: true };
|
|
100
|
-
companyRow.getCell(7).font = { bold: true };
|
|
120
|
+
const d1 = ws.getCell('D1');
|
|
121
|
+
d1.value = titleLabel;
|
|
122
|
+
d1.font = { bold: true, size: 18, name: TIMES };
|
|
123
|
+
d1.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
124
|
+
d1.border = allBordersBlack;
|
|
101
125
|
|
|
102
|
-
|
|
126
|
+
// G1 — No: BM-2-901-13
|
|
127
|
+
const g1 = ws.getCell('G1');
|
|
128
|
+
g1.value = 'No: BM-2-901-13';
|
|
129
|
+
g1.font = { size: 12, name: TIMES };
|
|
130
|
+
g1.alignment = { vertical: 'middle' };
|
|
131
|
+
g1.border = { top: thinBlack, left: thinBlack, right: thinBlack };
|
|
103
132
|
|
|
104
|
-
//
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
// G2 — Version
|
|
134
|
+
const g2 = ws.getCell('G2');
|
|
135
|
+
g2.value = `Version: ${getPackageVersion()}`;
|
|
136
|
+
g2.font = { size: 12, name: TIMES };
|
|
137
|
+
g2.alignment = { vertical: 'middle' };
|
|
138
|
+
g2.border = { left: thinBlack, right: thinBlack };
|
|
139
|
+
|
|
140
|
+
// G3 — Issue Date
|
|
141
|
+
const g3 = ws.getCell('G3');
|
|
142
|
+
g3.value = `Issue Date: ${issueDate}`;
|
|
143
|
+
g3.font = { size: 12, name: TIMES };
|
|
144
|
+
g3.alignment = { vertical: 'middle' };
|
|
145
|
+
g3.border = { left: thinBlack, right: thinBlack, bottom: thinBlack };
|
|
146
|
+
|
|
147
|
+
// A4 — SUN ASTERISK VIETNAM CO., LTD
|
|
148
|
+
const a4 = ws.getCell('A4');
|
|
149
|
+
a4.value = 'SUN ASTERISK VIETNAM CO., LTD';
|
|
150
|
+
a4.font = { bold: true, size: 10, name: TIMES };
|
|
151
|
+
a4.alignment = { vertical: 'middle' };
|
|
152
|
+
a4.border = allBordersBlack;
|
|
153
|
+
|
|
154
|
+
// G4 — ISO/IEC ...
|
|
155
|
+
const g4 = ws.getCell('G4');
|
|
156
|
+
g4.value = 'ISO/IEC 27001:2022 & ISO 9001:2015';
|
|
157
|
+
g4.font = { bold: true, size: 10, name: TIMES };
|
|
158
|
+
g4.alignment = { horizontal: 'right', vertical: 'middle', wrapText: true };
|
|
159
|
+
g4.border = allBordersBlack;
|
|
160
|
+
|
|
161
|
+
// -- Row 5: spacer (height auto-fit). --
|
|
162
|
+
|
|
163
|
+
// -- Row 6: Summary headers (cols C..H), lavender fill --
|
|
164
|
+
const sumLabels: Record<string, string> = {
|
|
165
|
+
C: 'Total TCs', D: 'Passed', E: 'Failed', F: 'Pending', G: 'N/A', H: 'Remaining',
|
|
166
|
+
};
|
|
167
|
+
for (const [col, label] of Object.entries(sumLabels)) {
|
|
168
|
+
const c = ws.getCell(`${col}6`);
|
|
169
|
+
c.value = label;
|
|
170
|
+
c.font = { bold: true, size: 10, color: BLACK, name: ARIAL_FONT };
|
|
171
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: LAVENDER };
|
|
172
|
+
c.alignment = { horizontal: 'center', vertical: 'top' };
|
|
173
|
+
c.border = allBordersBlack;
|
|
121
174
|
}
|
|
175
|
+
// No explicit height → Excel auto-fits this row's content.
|
|
122
176
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
summary.
|
|
130
|
-
summary.
|
|
131
|
-
summary.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
c
|
|
138
|
-
|
|
177
|
+
// -- Row 7: Counts (cols C..H), live formulas referencing the data area --
|
|
178
|
+
// Data table starts at row 15 (col header at row 14, divider at row 14? actually
|
|
179
|
+
// header at row 13 + divider at 14 + first data at 15). The formulas read col L
|
|
180
|
+
// (Test Result) for status counts, col A (TC ID) for total via COUNTA.
|
|
181
|
+
const COUNT_RANGE_END = 10000;
|
|
182
|
+
const r7Values: Record<string, ExcelJS.CellValue> = {
|
|
183
|
+
C: { formula: `COUNTA(A15:A${COUNT_RANGE_END})`, result: summary.total } as ExcelJS.CellFormulaValue,
|
|
184
|
+
D: { formula: `COUNTIF(L15:L${COUNT_RANGE_END},"Passed")`, result: summary.passed } as ExcelJS.CellFormulaValue,
|
|
185
|
+
E: { formula: `COUNTIF(L15:L${COUNT_RANGE_END},"Failed")`, result: summary.failed } as ExcelJS.CellFormulaValue,
|
|
186
|
+
F: { formula: `COUNTIF(L15:L${COUNT_RANGE_END},"Pending")`, result: summary.pending } as ExcelJS.CellFormulaValue,
|
|
187
|
+
G: { formula: `COUNTIF(L15:L${COUNT_RANGE_END},"N/A")`, result: summary.na } as ExcelJS.CellFormulaValue,
|
|
188
|
+
H: { formula: 'C7-D7-G7', result: summary.failed + summary.pending } as ExcelJS.CellFormulaValue,
|
|
189
|
+
};
|
|
190
|
+
for (const [col, val] of Object.entries(r7Values)) {
|
|
191
|
+
const c = ws.getCell(`${col}7`);
|
|
192
|
+
c.value = val;
|
|
193
|
+
c.font = { size: 10, color: BLACK, name: ARIAL_FONT };
|
|
194
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: WHITE };
|
|
195
|
+
c.alignment = { horizontal: 'center', vertical: 'top' };
|
|
196
|
+
c.border = allBordersBlack;
|
|
197
|
+
if (col === 'H') c.numFmt = '#,##0';
|
|
139
198
|
}
|
|
199
|
+
// No explicit height → Excel auto-fits.
|
|
140
200
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
201
|
+
// C8 has no value in the template but still needs a border so the band
|
|
202
|
+
// visually closes underneath "Total TCs".
|
|
203
|
+
const c8 = ws.getCell('C8');
|
|
204
|
+
c8.font = { size: 10, color: BLACK, name: ARIAL_FONT };
|
|
205
|
+
c8.fill = { type: 'pattern', pattern: 'solid', fgColor: WHITE };
|
|
206
|
+
c8.alignment = { horizontal: 'center', vertical: 'top' };
|
|
207
|
+
c8.border = allBordersBlack;
|
|
208
|
+
|
|
209
|
+
// -- Row 8: Percentages (cols D..H), formula = count / (Total − N/A) --
|
|
210
|
+
const r8Values: Record<string, ExcelJS.CellValue> = {
|
|
211
|
+
D: { formula: 'IFERROR(D7/(C7-G7),0)', result: summary.passed / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
|
|
212
|
+
E: { formula: 'IFERROR(E7/(C7-G7),0)', result: summary.failed / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
|
|
213
|
+
F: { formula: 'IFERROR(F7/(C7-G7),0)', result: summary.pending / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
|
|
214
|
+
G: { formula: 'IFERROR(G7/(C7-G7),0)', result: summary.na / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
|
|
215
|
+
H: { formula: 'IFERROR(H7/(C7-G7),0)', result: (summary.failed + summary.pending) / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
|
|
216
|
+
};
|
|
217
|
+
for (const [col, val] of Object.entries(r8Values)) {
|
|
218
|
+
const c = ws.getCell(`${col}8`);
|
|
219
|
+
c.value = val;
|
|
220
|
+
c.font = { size: 10, color: BLACK, name: ARIAL_FONT };
|
|
221
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: WHITE };
|
|
222
|
+
c.alignment = { horizontal: 'center', vertical: 'top' };
|
|
223
|
+
c.border = allBordersBlack;
|
|
224
|
+
c.numFmt = '0%';
|
|
156
225
|
}
|
|
226
|
+
// No explicit height → Excel auto-fits.
|
|
157
227
|
|
|
158
|
-
|
|
228
|
+
// After rows 1-8 are populated, append a blank row 9 spacer.
|
|
229
|
+
while (ws.rowCount < 9) ws.addRow([]);
|
|
159
230
|
|
|
160
231
|
const specRow = ws.addRow(['Spec/Design link:', specLink]);
|
|
161
232
|
specRow.getCell(1).font = { bold: true };
|
|
@@ -169,10 +240,10 @@ export function renderXlsx(
|
|
|
169
240
|
// -- Column header row (bold, filled, wrapped) --
|
|
170
241
|
const headerRow = ws.addRow([
|
|
171
242
|
'TC ID*',
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
'
|
|
175
|
-
'
|
|
243
|
+
'Screen/Function',
|
|
244
|
+
'Big item',
|
|
245
|
+
'Medium item',
|
|
246
|
+
'Test Object',
|
|
176
247
|
'Pre-condition',
|
|
177
248
|
'Test Data',
|
|
178
249
|
'Steps*',
|
|
@@ -222,10 +293,10 @@ export function renderXlsx(
|
|
|
222
293
|
for (const r of groupRows) {
|
|
223
294
|
const row = ws.addRow([
|
|
224
295
|
r.tcId,
|
|
225
|
-
r.category1,
|
|
226
296
|
'',
|
|
227
297
|
'',
|
|
228
298
|
'',
|
|
299
|
+
r.category1,
|
|
229
300
|
r.precondition,
|
|
230
301
|
r.testData,
|
|
231
302
|
r.steps,
|
|
@@ -278,6 +349,17 @@ export function renderXlsx(
|
|
|
278
349
|
to: { row: ws.rowCount, column: COL_COUNT },
|
|
279
350
|
};
|
|
280
351
|
|
|
352
|
+
// Sheet protection lets the picLocks on the embedded logo take effect
|
|
353
|
+
// (Excel honours image locks only when the sheet is protected). Empty
|
|
354
|
+
// password — QA can run Review → Unprotect Sheet if they need to edit.
|
|
355
|
+
// `objects: false` here is ExcelJS's inverted semantic that writes
|
|
356
|
+
// `objects="1"` in XML (= objects ARE locked).
|
|
357
|
+
(ws as unknown as { sheetProtection: object }).sheetProtection = {
|
|
358
|
+
sheet: true,
|
|
359
|
+
objects: false,
|
|
360
|
+
scenarios: false,
|
|
361
|
+
};
|
|
362
|
+
|
|
281
363
|
return wb;
|
|
282
364
|
}
|
|
283
365
|
|
|
@@ -293,9 +375,41 @@ export async function writeXlsx(
|
|
|
293
375
|
const outDir = path.join(cwd, 'qa', 'deliverables');
|
|
294
376
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
295
377
|
const outPath = path.join(outDir, `${screen}-testcases.xlsx`);
|
|
296
|
-
await wb.xlsx.
|
|
378
|
+
const buffer = await wb.xlsx.writeBuffer();
|
|
379
|
+
const locked = await lockEmbeddedImages(Buffer.from(buffer as ArrayBuffer));
|
|
380
|
+
fs.writeFileSync(outPath, locked);
|
|
297
381
|
return outPath;
|
|
298
382
|
}
|
|
299
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Post-process the workbook buffer: extend `<a:picLocks>` on every drawing
|
|
386
|
+
* with `noMove`, `noResize`, `noSelect`. Combined with the sheet protection
|
|
387
|
+
* above, the embedded logo can't be dragged, resized, or selected via the UI.
|
|
388
|
+
*/
|
|
389
|
+
export async function lockEmbeddedImages(buffer: Buffer): Promise<Buffer> {
|
|
390
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
391
|
+
const drawingFiles = Object.keys(zip.files).filter(
|
|
392
|
+
(name) => /^xl\/drawings\/drawing\d+\.xml$/.test(name)
|
|
393
|
+
);
|
|
394
|
+
for (const name of drawingFiles) {
|
|
395
|
+
const file = zip.file(name);
|
|
396
|
+
if (!file) continue;
|
|
397
|
+
const xml = await file.async('string');
|
|
398
|
+
const patched = xml.replace(
|
|
399
|
+
/<a:picLocks([^/]*)\/>/g,
|
|
400
|
+
(_match, attrs: string) => {
|
|
401
|
+
const has = (k: string) => new RegExp(`\\b${k}=`).test(attrs);
|
|
402
|
+
const additions = ['noMove', 'noResize', 'noSelect']
|
|
403
|
+
.filter((k) => !has(k))
|
|
404
|
+
.map((k) => ` ${k}="1"`)
|
|
405
|
+
.join('');
|
|
406
|
+
return `<a:picLocks${attrs}${additions}/>`;
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
zip.file(name, patched);
|
|
410
|
+
}
|
|
411
|
+
return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
412
|
+
}
|
|
413
|
+
|
|
300
414
|
void applyBorder;
|
|
301
415
|
void ({} as AnyRow);
|
|
@@ -450,8 +450,9 @@ export class ProjectInitializer {
|
|
|
450
450
|
console.log(`📦 Installing ${missingDeps.join(', ')}...\n`);
|
|
451
451
|
execSync(`npm install -D ${missingDeps.join(' ')}`, execOpts);
|
|
452
452
|
|
|
453
|
-
console.log('\n🎭 Installing
|
|
454
|
-
execSync('npx playwright install', execOpts);
|
|
453
|
+
console.log('\n🎭 Installing Chromium (default browser)...\n');
|
|
454
|
+
execSync('npx playwright install --with-deps chromium', execOpts);
|
|
455
|
+
console.log('\n💡 To install other browsers: npm run install:browsers -- firefox webkit\n');
|
|
455
456
|
}
|
|
456
457
|
|
|
457
458
|
/**
|
|
@@ -477,7 +478,7 @@ export class ProjectInitializer {
|
|
|
477
478
|
'test:ui': 'playwright test specs/generated/ --ui',
|
|
478
479
|
'report': 'playwright show-report',
|
|
479
480
|
'generate': 'sungen generate --all',
|
|
480
|
-
'install:browsers': 'npx playwright install
|
|
481
|
+
'install:browsers': 'npx playwright install',
|
|
481
482
|
};
|
|
482
483
|
|
|
483
484
|
let added = 0;
|
|
@@ -13,6 +13,8 @@ import { defineConfig, devices } from '@playwright/test';
|
|
|
13
13
|
*/
|
|
14
14
|
export default defineConfig({
|
|
15
15
|
testDir: './specs/generated',
|
|
16
|
+
/* Output directory for test artifacts (screenshots, videos, traces). */
|
|
17
|
+
outputDir: './test-results',
|
|
16
18
|
/* Run tests in files in parallel */
|
|
17
19
|
fullyParallel: true,
|
|
18
20
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
@@ -66,14 +66,10 @@ const test = base.extend<{
|
|
|
66
66
|
}>({
|
|
67
67
|
screenshotOnFailure: [false, { option: true }],
|
|
68
68
|
|
|
69
|
-
page: async ({
|
|
70
|
-
const context = storageState
|
|
71
|
-
? await browser.newContext({ storageState })
|
|
72
|
-
: await browser.newContext();
|
|
69
|
+
page: async ({ context }, use) => {
|
|
73
70
|
const page = await context.newPage();
|
|
74
71
|
await use(page);
|
|
75
72
|
await page.close();
|
|
76
|
-
await context.close();
|
|
77
73
|
},
|
|
78
74
|
|
|
79
75
|
_autoScreenshot: [async ({ page, screenshotOnFailure }, use, testInfo) => {
|