@sun-asterisk/sungen 2.6.10 → 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.
@@ -12,6 +12,7 @@
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';
17
18
  import { SUN_LOGO_PNG_BASE64 } from './sun-logo';
@@ -54,7 +55,7 @@ export function renderXlsx(
54
55
 
55
56
  // -- Column widths matching template_report.xlsx (Sample sheet) --
56
57
  ws.columns = [
57
- { width: 6.38 }, // A — TC ID
58
+ { width: 20 }, // A — TC ID (wider to fit long flow IDs like FLOW-KUDO-…)
58
59
  { width: 12.5 }, // B — Screen/Function
59
60
  { width: 15.38 }, // C — Big item
60
61
  { width: 16.25 }, // D — Medium item
@@ -92,28 +93,24 @@ export function renderXlsx(
92
93
  ws.mergeCells('A4:F4');
93
94
  ws.mergeCells('G4:H4');
94
95
 
95
- // A1 (logo band, merged A1:C3) — give it bordering + embed the Sun* logo.
96
+ // A1 (logo band, merged A1:C3) — border + embedded Sun* logo (base64 inline).
96
97
  const a1 = ws.getCell('A1');
97
98
  a1.alignment = { horizontal: 'center', vertical: 'middle' };
98
99
  a1.border = allBordersBlack;
99
100
 
100
- // Embed Sun* logo using a oneCellAnchor + explicit pixel `ext`, exactly
101
- // like the template. tl is offset into the merged band so the logo lands
102
- // centred in A1:C3 instead of pinned to the top-left corner.
103
101
  try {
104
102
  const imageId = wb.addImage({
105
103
  buffer: Buffer.from(SUN_LOGO_PNG_BASE64, 'base64') as unknown as ExcelJS.Buffer,
106
104
  extension: 'png',
107
105
  });
108
- // A1:C3 240 × 60 px. For a 90×50 logo centred horizontally / vertically:
109
- // left margin = (240 90)/2 ≈ 75 px 9525 EMU/px 714375 EMU
110
- // top margin = (60 − 50)/2 5 px 47625 EMU
111
- // ExcelJS's fractional `col`/`row` clamps to a small EMU range, so we
112
- // bypass it and write `native*` fields directly (the underlying XML uses
113
- // the same field names).
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:C3350 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.
114
111
  ws.addImage(imageId, {
115
- tl: { nativeCol: 0, nativeColOff: 714375, nativeRow: 0, nativeRowOff: 47625 } as unknown as ExcelJS.Anchor,
116
- ext: { width: 90, height: 50 },
112
+ tl: { nativeCol: 0, nativeColOff: 1237500, nativeRow: 0, nativeRowOff: 42862 } as unknown as ExcelJS.Anchor,
113
+ ext: { width: 90, height: 51 },
117
114
  editAs: 'oneCell',
118
115
  } as unknown as ExcelJS.ImageRange);
119
116
  } catch { /* logo is decorative — never block export */ }
@@ -161,8 +158,7 @@ export function renderXlsx(
161
158
  g4.alignment = { horizontal: 'right', vertical: 'middle', wrapText: true };
162
159
  g4.border = allBordersBlack;
163
160
 
164
- // -- Row 5: spacer (height 12) --
165
- ws.getRow(5).height = 12;
161
+ // -- Row 5: spacer (height auto-fit). --
166
162
 
167
163
  // -- Row 6: Summary headers (cols C..H), lavender fill --
168
164
  const sumLabels: Record<string, string> = {
@@ -176,7 +172,7 @@ export function renderXlsx(
176
172
  c.alignment = { horizontal: 'center', vertical: 'top' };
177
173
  c.border = allBordersBlack;
178
174
  }
179
- ws.getRow(6).height = 12;
175
+ // No explicit height Excel auto-fits this row's content.
180
176
 
181
177
  // -- Row 7: Counts (cols C..H), live formulas referencing the data area --
182
178
  // Data table starts at row 15 (col header at row 14, divider at row 14? actually
@@ -200,7 +196,7 @@ export function renderXlsx(
200
196
  c.border = allBordersBlack;
201
197
  if (col === 'H') c.numFmt = '#,##0';
202
198
  }
203
- ws.getRow(7).height = 12;
199
+ // No explicit height Excel auto-fits.
204
200
 
205
201
  // C8 has no value in the template but still needs a border so the band
206
202
  // visually closes underneath "Total TCs".
@@ -227,7 +223,7 @@ export function renderXlsx(
227
223
  c.border = allBordersBlack;
228
224
  c.numFmt = '0%';
229
225
  }
230
- ws.getRow(8).height = 12;
226
+ // No explicit height Excel auto-fits.
231
227
 
232
228
  // After rows 1-8 are populated, append a blank row 9 spacer.
233
229
  while (ws.rowCount < 9) ws.addRow([]);
@@ -353,6 +349,17 @@ export function renderXlsx(
353
349
  to: { row: ws.rowCount, column: COL_COUNT },
354
350
  };
355
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
+
356
363
  return wb;
357
364
  }
358
365
 
@@ -368,9 +375,41 @@ export async function writeXlsx(
368
375
  const outDir = path.join(cwd, 'qa', 'deliverables');
369
376
  if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
370
377
  const outPath = path.join(outDir, `${screen}-testcases.xlsx`);
371
- await wb.xlsx.writeFile(outPath);
378
+ const buffer = await wb.xlsx.writeBuffer();
379
+ const locked = await lockEmbeddedImages(Buffer.from(buffer as ArrayBuffer));
380
+ fs.writeFileSync(outPath, locked);
372
381
  return outPath;
373
382
  }
374
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
+
375
414
  void applyBorder;
376
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 Playwright browsers...\n');
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 chromium',
481
+ 'install:browsers': 'npx playwright install',
481
482
  };
482
483
 
483
484
  let added = 0;