@sun-asterisk/sungen 3.2.6 → 3.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/cli/commands/delivery.d.ts +25 -0
  2. package/dist/cli/commands/delivery.d.ts.map +1 -1
  3. package/dist/cli/commands/delivery.js +30 -47
  4. package/dist/cli/commands/delivery.js.map +1 -1
  5. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  6. package/dist/dashboard/snapshot-builder.js +28 -6
  7. package/dist/dashboard/snapshot-builder.js.map +1 -1
  8. package/dist/dashboard/templates/index.html +20 -20
  9. package/dist/dashboard/types.d.ts +18 -0
  10. package/dist/dashboard/types.d.ts.map +1 -1
  11. package/dist/exporters/api-catalog-loader.d.ts +18 -0
  12. package/dist/exporters/api-catalog-loader.d.ts.map +1 -0
  13. package/dist/exporters/api-catalog-loader.js +77 -0
  14. package/dist/exporters/api-catalog-loader.js.map +1 -0
  15. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  16. package/dist/exporters/api-testcase-formatter.js +80 -4
  17. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  18. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  19. package/dist/exporters/csv-exporter.js +6 -1
  20. package/dist/exporters/csv-exporter.js.map +1 -1
  21. package/dist/exporters/json-exporter.d.ts +10 -1
  22. package/dist/exporters/json-exporter.d.ts.map +1 -1
  23. package/dist/exporters/json-exporter.js +34 -2
  24. package/dist/exporters/json-exporter.js.map +1 -1
  25. package/dist/exporters/playwright-report-parser.d.ts +14 -0
  26. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  27. package/dist/exporters/playwright-report-parser.js +30 -0
  28. package/dist/exporters/playwright-report-parser.js.map +1 -1
  29. package/dist/exporters/types.d.ts +5 -0
  30. package/dist/exporters/types.d.ts.map +1 -1
  31. package/dist/exporters/xlsx-exporter.d.ts +18 -17
  32. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  33. package/dist/exporters/xlsx-exporter.js +25 -595
  34. package/dist/exporters/xlsx-exporter.js.map +1 -1
  35. package/dist/exporters/xlsx-report-builder.d.ts +196 -0
  36. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -0
  37. package/dist/exporters/xlsx-report-builder.js +653 -0
  38. package/dist/exporters/xlsx-report-builder.js.map +1 -0
  39. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +3 -3
  40. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +1 -1
  41. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +2 -2
  42. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +1 -1
  43. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +2 -2
  44. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +2 -2
  45. package/dist/generators/test-generator/step-mapper.d.ts +6 -2
  46. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  47. package/dist/generators/test-generator/step-mapper.js +10 -6
  48. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  49. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +8 -0
  50. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
  51. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +3 -0
  52. package/dist/orchestrator/templates/specs-api.d.ts +14 -2
  53. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  54. package/dist/orchestrator/templates/specs-api.js +38 -14
  55. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  56. package/dist/orchestrator/templates/specs-api.ts +43 -14
  57. package/package.json +3 -3
  58. package/src/cli/commands/delivery.ts +32 -46
  59. package/src/dashboard/snapshot-builder.ts +31 -7
  60. package/src/dashboard/templates/index.html +20 -20
  61. package/src/dashboard/types.ts +19 -0
  62. package/src/exporters/api-catalog-loader.ts +44 -0
  63. package/src/exporters/api-testcase-formatter.ts +77 -4
  64. package/src/exporters/csv-exporter.ts +6 -1
  65. package/src/exporters/json-exporter.ts +46 -4
  66. package/src/exporters/playwright-report-parser.ts +27 -0
  67. package/src/exporters/types.ts +5 -0
  68. package/src/exporters/xlsx-exporter.ts +34 -658
  69. package/src/exporters/xlsx-report-builder.ts +806 -0
  70. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +3 -3
  71. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +1 -1
  72. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +2 -2
  73. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +1 -1
  74. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +2 -2
  75. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +2 -2
  76. package/src/generators/test-generator/step-mapper.ts +10 -6
  77. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +8 -0
  78. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
  79. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +3 -0
  80. package/src/orchestrator/templates/specs-api.ts +43 -14
  81. package/dist/cli/types.d.ts +0 -9
  82. package/dist/cli/types.d.ts.map +0 -1
  83. package/dist/cli/types.js +0 -7
  84. package/dist/cli/types.js.map +0 -1
  85. package/dist/generators/gherkin-parser/selector-extractor.d.ts +0 -37
  86. package/dist/generators/gherkin-parser/selector-extractor.d.ts.map +0 -1
  87. package/dist/generators/gherkin-parser/selector-extractor.js +0 -108
  88. package/dist/generators/gherkin-parser/selector-extractor.js.map +0 -1
  89. package/dist/generators/test-generator/types.d.ts +0 -25
  90. package/dist/generators/test-generator/types.d.ts.map +0 -1
  91. package/dist/generators/test-generator/types.js +0 -7
  92. package/dist/generators/test-generator/types.js.map +0 -1
  93. package/dist/generators/types.d.ts +0 -119
  94. package/dist/generators/types.d.ts.map +0 -1
  95. package/dist/generators/types.js +0 -48
  96. package/dist/generators/types.js.map +0 -1
  97. package/dist/orchestrator/figma/node-path-collapser.d.ts +0 -16
  98. package/dist/orchestrator/figma/node-path-collapser.d.ts.map +0 -1
  99. package/dist/orchestrator/figma/node-path-collapser.js +0 -37
  100. package/dist/orchestrator/figma/node-path-collapser.js.map +0 -1
  101. package/src/cli/types.ts +0 -9
  102. package/src/generators/gherkin-parser/selector-extractor.ts +0 -142
  103. package/src/generators/test-generator/types.ts +0 -26
  104. package/src/generators/types.ts +0 -227
  105. package/src/orchestrator/figma/node-path-collapser.ts +0 -38
@@ -12,7 +12,7 @@ import { parseFeatureMetadata } from '../../exporters/feature-parser';
12
12
  import { parseSpecFile } from '../../exporters/spec-parser';
13
13
  import { loadTestData } from '../../exporters/test-data-resolver';
14
14
  import { loadSelectorKeyMap } from '../../exporters/selector-key-resolver';
15
- import { loadPlaywrightReport } from '../../exporters/playwright-report-parser';
15
+ import { loadPlaywrightReport, resolveResultsPath as resolveResultsPathShared } from '../../exporters/playwright-report-parser';
16
16
  import { mergeFeatureAndSpec } from '../../exporters/scenario-merger';
17
17
  import {
18
18
  buildTestCaseRows,
@@ -21,6 +21,7 @@ import {
21
21
  writeCsv,
22
22
  } from '../../exporters/csv-exporter';
23
23
  import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet } from '../../exporters/xlsx-exporter';
24
+ import { loadApiCatalog } from '../../exporters/api-catalog-loader';
24
25
  import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
25
26
  import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
26
27
 
@@ -57,7 +58,7 @@ type UnitKind = 'screen' | 'flow' | 'api';
57
58
  /** qa/ subfolder for a unit kind. */
58
59
  const qaParent = (kind: UnitKind): string => (kind === 'flow' ? 'flows' : kind === 'api' ? 'api' : 'screens');
59
60
 
60
- interface DeliveryTarget {
61
+ export interface DeliveryTarget {
61
62
  screen: string;
62
63
  featureBaseName: string;
63
64
  /** Alias of `featureBaseName` — preserves the old `target.name` call sites. */
@@ -136,35 +137,8 @@ function qaDir(cwd: string, target: DeliveryTarget): string {
136
137
  return path.join(cwd, 'qa', qaParent(target.kind), target.screen);
137
138
  }
138
139
 
139
- /**
140
- * Load the apis.yaml catalog for an api-kind unit, merging the shared catalog under
141
- * the unit catalog. The runtime resolves an endpoint unit-scope-first then falls back
142
- * to the shared catalog (driver-api api-catalog.ts), so an endpoint defined only in
143
- * `qa/api/apis.yaml` still runs — we must load it too, or its request block would come
144
- * back empty (the very bug this feature fixes). Unit wins on a name collision.
145
- * Returns an empty object when neither file is present (graceful degradation).
146
- */
147
- function loadApiCatalog(cwd: string, target: DeliveryTarget): Record<string, ApiCatalogEntry> {
148
- if (target.kind !== 'api') return {};
149
- const shared = readApiCatalogFile(path.join(cwd, 'qa', 'api', 'apis.yaml'));
150
- const unit = readApiCatalogFile(path.join(cwd, 'qa', 'api', target.screen, 'api', 'apis.yaml'));
151
- return { ...shared, ...unit };
152
- }
153
-
154
- /** Parse one apis.yaml file into an endpoint map, unwrapping the optional `apis:` wrapper. */
155
- function readApiCatalogFile(catalogPath: string): Record<string, ApiCatalogEntry> {
156
- if (!fs.existsSync(catalogPath)) return {};
157
- try {
158
- const parsed = parseYaml(fs.readFileSync(catalogPath, 'utf-8')) as Record<string, unknown> | null;
159
- if (parsed && typeof parsed === 'object') {
160
- // The runtime loader accepts both a flat map and an `apis:` wrapper — unwrap it
161
- // here too so both loaders read the same file identically.
162
- const root = parsed.apis && typeof parsed.apis === 'object' ? parsed.apis : parsed;
163
- return root as Record<string, ApiCatalogEntry>;
164
- }
165
- } catch { /* malformed yaml — skip */ }
166
- return {};
167
- }
140
+ // apis.yaml catalog loading moved to ../../exporters/api-catalog-loader so the
141
+ // dashboard snapshot builder loads the identical contract (shared + unit merge).
168
142
 
169
143
  function generatedDir(cwd: string, target: DeliveryTarget): string {
170
144
  const sub = target.kind === 'flow' ? path.join('flows', target.screen) : target.kind === 'api' ? path.join('api', target.screen) : target.screen;
@@ -188,17 +162,9 @@ function generatedDir(cwd: string, target: DeliveryTarget): string {
188
162
  * `SUNGEN_ENV` is set, so each locale run keeps its own results file.
189
163
  */
190
164
  function resolveResultsPath(cwd: string, target: DeliveryTarget): string | null {
191
- const genDir = generatedDir(cwd, target);
192
- const env = process.env.SUNGEN_ENV;
193
- const candidates: string[] = [];
194
- if (env) candidates.push(path.join(genDir, `${target.featureBaseName}-test-result.${env}.json`));
195
- candidates.push(path.join(genDir, `${target.featureBaseName}-test-result.json`));
196
- if (env) candidates.push(path.join(cwd, 'test-results', `results.${env}.json`));
197
- candidates.push(path.join(cwd, 'test-results', 'results.json'));
198
- for (const c of candidates) {
199
- if (fs.existsSync(c)) return c;
200
- }
201
- return null;
165
+ // Shared with the dashboard snapshot builder (same candidate order incl. the
166
+ // test-results/results.json fallback) so the two surfaces can't drift.
167
+ return resolveResultsPathShared(cwd, generatedDir(cwd, target), target.featureBaseName);
202
168
  }
203
169
 
204
170
  /**
@@ -290,7 +256,7 @@ function readFlowPagePath(selectorsFile: string): string | undefined {
290
256
  return undefined;
291
257
  }
292
258
 
293
- function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
259
+ export function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
294
260
  const base = qaDir(cwd, target);
295
261
  const genBase = generatedDir(cwd, target);
296
262
  const featureFile = path.join(base, 'features', `${target.featureBaseName}.feature`);
@@ -300,7 +266,13 @@ function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
300
266
  const resultsFile = resolveResultsPath(cwd, target);
301
267
 
302
268
  const featureOk = checkFeatureReal(featureFile);
303
- const testDataOk = checkTestDataHasVars(testDataFile);
269
+ // test-data is only REQUIRED when the feature actually references a
270
+ // {{var}} placeholder. A static screen (e.g. a fixed bottom-navigation bar)
271
+ // legitimately has zero test-data variables — blocking its export was a
272
+ // false positive (issue: preflight blocked static screens).
273
+ const testDataOk = featureReferencesTestData(featureFile)
274
+ ? checkTestDataHasVars(testDataFile)
275
+ : true;
304
276
  // API units have no DOM → no selectors; the catalog (apis.yaml) is the contract. N/A, not missing.
305
277
  const selectorsOk = target.kind === 'api' ? true : checkSelectorsHasEntries(selectorsFile, target.featureBaseName);
306
278
  const specOk = fs.existsSync(specFile);
@@ -367,6 +339,20 @@ function checkTestDataHasVars(testDataFile: string): boolean {
367
339
  return lines.length > 0;
368
340
  }
369
341
 
342
+ /**
343
+ * True when the .feature references at least one test-data variable — a
344
+ * `{{var}}` placeholder that is not a runtime-generated `{{$...}}` var
345
+ * (e.g. `{{$uuid}}`, resolved at execution time, never from test-data.yaml).
346
+ * Comment-only or reference-free features need no test-data entries.
347
+ */
348
+ function featureReferencesTestData(featureFile: string): boolean {
349
+ if (!fs.existsSync(featureFile)) return false;
350
+ const content = fs.readFileSync(featureFile, 'utf-8');
351
+ // Strip Gherkin comment lines so a {{var}} inside a `# ...` note doesn't count.
352
+ const code = content.split('\n').filter((l) => !l.trim().startsWith('#')).join('\n');
353
+ return /\{\{\s*[^$}\s][^}]*\}\}/.test(code);
354
+ }
355
+
370
356
  function checkSelectorsHasEntries(selectorsFile: string, screen: string): boolean {
371
357
  if (!fs.existsSync(selectorsFile)) return false;
372
358
  const content = fs.readFileSync(selectorsFile, 'utf-8');
@@ -376,7 +362,7 @@ function checkSelectorsHasEntries(selectorsFile: string, screen: string): boolea
376
362
  return topLevelKeys > 1;
377
363
  }
378
364
 
379
- function hasBlockingMissing(p: PreflightCheck): boolean {
365
+ export function hasBlockingMissing(p: PreflightCheck): boolean {
380
366
  return !p.featureOk || !p.testDataOk || !p.selectorsOk || !p.specOk;
381
367
  }
382
368
 
@@ -477,7 +463,7 @@ async function exportTarget(
477
463
  const explicitEnv = process.env.SUNGEN_ENV;
478
464
 
479
465
  // For api-kind units, load the endpoint catalog so we can add the API detail sheet.
480
- const apiCatalog = loadApiCatalog(cwd, target);
466
+ const apiCatalog = loadApiCatalog(cwd, target.kind, target.screen);
481
467
  const hasApiCatalog = target.kind === 'api' && Object.keys(apiCatalog).length > 0;
482
468
 
483
469
  // Decide between single-locale and aggregated multi-locale flows.
@@ -19,12 +19,14 @@ import { parseFeatureMetadata } from '../exporters/feature-parser';
19
19
  import { parseSpecFile } from '../exporters/spec-parser';
20
20
  import { loadTestData } from '../exporters/test-data-resolver';
21
21
  import { loadSelectorKeyMap } from '../exporters/selector-key-resolver';
22
- import { loadPlaywrightReport } from '../exporters/playwright-report-parser';
22
+ import { loadPlaywrightReport, resolveResultsPath } from '../exporters/playwright-report-parser';
23
23
  import { mergeFeatureAndSpec } from '../exporters/scenario-merger';
24
24
  import { buildScreenSnapshot } from '../exporters/json-exporter';
25
+ import { loadApiCatalog } from '../exporters/api-catalog-loader';
26
+ import { buildApiDetailRows } from '../exporters/xlsx-exporter';
25
27
  import { resolvePlatformAppId } from '../exporters/mobile-app-id';
26
28
  import { getPackageVersion } from '../exporters/package-info';
27
- import { EnvironmentInfo } from '../exporters/types';
29
+ import { ApiCatalogEntry, EnvironmentInfo } from '../exporters/types';
28
30
  import {
29
31
  AggregateSummary,
30
32
  DashboardSnapshot,
@@ -126,9 +128,13 @@ function buildOneScreen(
126
128
  const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : undefined;
127
129
 
128
130
  const mobile = isMobileProject(cwd);
131
+ // api-kind units: load the endpoint catalog once per target — the same
132
+ // shared+unit merge delivery.ts uses — so Steps/Expected cells and the API
133
+ // detail sheet match the delivery CSV/XLSX exactly.
134
+ const apiCatalog = loadApiCatalog(cwd, target.kind, target.name);
129
135
  const features: FeatureSnapshot[] = [];
130
136
  for (const featureBaseName of featureBaseNames) {
131
- features.push(...buildOneFeature(cwd, target, featureBaseName, env, specLink, mobile));
137
+ features.push(...buildOneFeature(cwd, target, featureBaseName, env, specLink, mobile, apiCatalog));
132
138
  }
133
139
  if (features.length === 0) return null;
134
140
 
@@ -183,6 +189,7 @@ function buildOneFeature(
183
189
  env: EnvironmentInfo,
184
190
  specLink: string | undefined,
185
191
  mobile: boolean,
192
+ apiCatalog: Record<string, ApiCatalogEntry>,
186
193
  ): FeatureSnapshot[] {
187
194
  const base = qaDir(cwd, target);
188
195
  const genBase = generatedDir(cwd, target);
@@ -210,7 +217,7 @@ function buildOneFeature(
210
217
  // Discover locale variants by scanning <feature>-test-result*.json files.
211
218
  // Always includes base ('') as the first entry, even if its results file
212
219
  // is missing — UI still needs to render the feature.
213
- const variants = discoverLocaleVariants(genBase, featureBaseName);
220
+ const variants = discoverLocaleVariants(cwd, genBase, featureBaseName);
214
221
 
215
222
  // Build one per-variant ScreenSnapshot (test-data overlay + results + OS-precise
216
223
  // env column). Reused by both the web/i18n (locale-axis) and mobile (platform-axis) paths.
@@ -233,10 +240,21 @@ function buildOneFeature(
233
240
  env,
234
241
  featureBaseName,
235
242
  selectorKeyMap,
243
+ kind: target.kind,
244
+ apiCatalog,
236
245
  });
237
246
  };
238
247
  type Built = ReturnType<typeof buildVariant>;
239
248
 
249
+ // API detail sheet rows — the identical buildApiDetailRows call delivery.ts
250
+ // makes before addApiDetailSheet, precomputed so the (browser-side) XLSX
251
+ // export just passes them to the shared builder. Endpoint contract is
252
+ // locale-invariant → computed once per feature.
253
+ const apiDetailRows = target.kind === 'api' && Object.keys(apiCatalog).length > 0
254
+ ? buildApiDetailRows(apiCatalog, feature.scenarios)
255
+ : [];
256
+ const apiDetail = apiDetailRows.length > 0 ? apiDetailRows : undefined;
257
+
240
258
  // -- Mobile (platform axis): the per-OS runs (android / ios) are their own
241
259
  // FeatureSnapshot entries — one per OS, `platform` tagged — NOT locale tabs.
242
260
  // Locale overlays for a given OS (e.g. `android.ja`) nest inside that OS's
@@ -272,6 +290,7 @@ function buildOneFeature(
272
290
  platform: os,
273
291
  executedPlatforms,
274
292
  locales: localeSnaps.length >= 2 ? localeSnaps : undefined,
293
+ apiDetail,
275
294
  });
276
295
  }
277
296
  if (feats.length > 0) return feats;
@@ -318,6 +337,7 @@ function buildOneFeature(
318
337
  // Only expose `locales` array when 2+ variants exist — single-locale
319
338
  // features stay slimmer and UI can skip the locale tab UI entirely.
320
339
  locales: locales.length >= 2 ? locales : undefined,
340
+ apiDetail,
321
341
  }];
322
342
  }
323
343
 
@@ -342,15 +362,19 @@ interface FeatureLocaleVariant {
342
362
  * dashboard and the CSV/XLSX delivery agree on what locales exist for a
343
363
  * given feature.
344
364
  */
345
- function discoverLocaleVariants(genDir: string, featureBaseName: string): FeatureLocaleVariant[] {
365
+ function discoverLocaleVariants(cwd: string, genDir: string, featureBaseName: string): FeatureLocaleVariant[] {
346
366
  const prefix = `${featureBaseName}-test-result`;
347
367
  const variants: FeatureLocaleVariant[] = [];
348
368
 
349
- const basePath = path.join(genDir, `${prefix}.json`);
369
+ // Base variant resolves through the SHARED candidate chain (per-feature file
370
+ // first, then the global test-results/results.json playwright writes by
371
+ // default) — the same resolution delivery.ts uses. Without the fallback the
372
+ // dashboard showed every scenario Pending on projects that only have the
373
+ // global results file, while delivery correctly reported Passed.
350
374
  variants.push({
351
375
  locale: '',
352
376
  displayCode: DEFAULT_BASE_LOCALE.toUpperCase(),
353
- resultsPath: fs.existsSync(basePath) ? basePath : null,
377
+ resultsPath: resolveResultsPath(cwd, genDir, featureBaseName),
354
378
  });
355
379
 
356
380
  if (fs.existsSync(genDir)) {