@sun-asterisk/sungen 2.0.3 → 2.1.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.
Files changed (86) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/init.d.ts.map +1 -1
  3. package/dist/cli/commands/init.js +3 -2
  4. package/dist/cli/commands/init.js.map +1 -1
  5. package/dist/cli/index.js +1 -1
  6. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +6 -1
  7. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  8. package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +0 -4
  9. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -0
  10. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +11 -0
  11. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/toggle-action.hbs +1 -0
  12. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/loading-assertion.hbs +2 -0
  13. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +2 -0
  14. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/sorted-assertion.hbs +2 -0
  15. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +25 -0
  16. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  17. package/dist/generators/test-generator/code-generator.js +41 -3
  18. package/dist/generators/test-generator/code-generator.js.map +1 -1
  19. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
  20. package/dist/generators/test-generator/patterns/assertion-patterns.js +58 -6
  21. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
  22. package/dist/generators/test-generator/patterns/form-patterns.js +3 -3
  23. package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
  24. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
  25. package/dist/generators/test-generator/patterns/interaction-patterns.js +86 -1
  26. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
  27. package/dist/generators/test-generator/template-engine.d.ts +6 -0
  28. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  29. package/dist/generators/test-generator/template-engine.js +1 -0
  30. package/dist/generators/test-generator/template-engine.js.map +1 -1
  31. package/dist/orchestrator/project-initializer.d.ts +3 -6
  32. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  33. package/dist/orchestrator/project-initializer.js +62 -58
  34. package/dist/orchestrator/project-initializer.js.map +1 -1
  35. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +42 -0
  36. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +60 -0
  37. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +59 -0
  38. package/dist/orchestrator/templates/ai-instructions/claude-config.md +90 -0
  39. package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +27 -0
  40. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +127 -0
  41. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +94 -0
  42. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +41 -0
  43. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +59 -0
  44. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +58 -0
  45. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +90 -0
  46. package/dist/orchestrator/templates/ai-instructions/copilot-skill-error-mapping.md +27 -0
  47. package/dist/orchestrator/templates/ai-instructions/copilot-skill-gherkin-syntax.md +127 -0
  48. package/dist/orchestrator/templates/ai-instructions/copilot-skill-selector-keys.md +94 -0
  49. package/dist/orchestrator/templates/readme.md +42 -38
  50. package/docs/gherkin standards/gherkin-core-standard.md +141 -90
  51. package/docs/gherkin standards/gherkin-core-standard.vi.md +264 -54
  52. package/package.json +2 -2
  53. package/src/cli/commands/init.ts +3 -2
  54. package/src/cli/index.ts +1 -1
  55. package/src/generators/test-generator/adapters/adapter-interface.ts +7 -1
  56. package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +0 -4
  57. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -0
  58. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +11 -0
  59. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/toggle-action.hbs +1 -0
  60. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/loading-assertion.hbs +2 -0
  61. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +2 -0
  62. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/sorted-assertion.hbs +2 -0
  63. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +25 -0
  64. package/src/generators/test-generator/code-generator.ts +50 -8
  65. package/src/generators/test-generator/patterns/assertion-patterns.ts +62 -6
  66. package/src/generators/test-generator/patterns/form-patterns.ts +3 -3
  67. package/src/generators/test-generator/patterns/interaction-patterns.ts +93 -1
  68. package/src/generators/test-generator/template-engine.ts +4 -0
  69. package/src/orchestrator/project-initializer.ts +74 -58
  70. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +42 -0
  71. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +60 -0
  72. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +59 -0
  73. package/src/orchestrator/templates/ai-instructions/claude-config.md +90 -0
  74. package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +27 -0
  75. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +127 -0
  76. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +94 -0
  77. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +41 -0
  78. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +59 -0
  79. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +58 -0
  80. package/src/orchestrator/templates/ai-instructions/copilot-config.md +90 -0
  81. package/src/orchestrator/templates/ai-instructions/copilot-skill-error-mapping.md +27 -0
  82. package/src/orchestrator/templates/ai-instructions/copilot-skill-gherkin-syntax.md +127 -0
  83. package/src/orchestrator/templates/ai-instructions/copilot-skill-selector-keys.md +94 -0
  84. package/src/orchestrator/templates/readme.md +42 -38
  85. package/dist/orchestrator/templates/ai-rules.md +0 -189
  86. package/src/orchestrator/templates/ai-rules.md +0 -189
@@ -229,7 +229,8 @@ export class CodeGenerator {
229
229
 
230
230
  // Generate all scenarios with feature tags for inheritance
231
231
  // Skip scenarios tagged with @manual
232
- const scenarios: string[] = [];
232
+ // Track auth role per scenario for grouping
233
+ const renderedScenarios: Array<{ code: string; authRole?: string }> = [];
233
234
  for (const scenario of feature.scenarios) {
234
235
  if (isManual(scenario.tags)) {
235
236
  if (this.options.verbose) {
@@ -237,22 +238,63 @@ export class CodeGenerator {
237
238
  }
238
239
  continue;
239
240
  }
240
- scenarios.push(
241
- await this.generateScenario(
242
- scenario,
243
- !!feature.background,
244
- feature.tags || []
245
- )
241
+
242
+ // Resolve auth tags for @extend scenarios (same logic as generateScenario)
243
+ let authFeatureTags = feature.tags || [];
244
+ if (scenario.extendsName) {
245
+ const baseScenario = this.stepsRegistry.get(scenario.extendsName);
246
+ if (baseScenario) {
247
+ authFeatureTags = [...baseScenario.tags, ...authFeatureTags];
248
+ }
249
+ }
250
+ const authRole = getEffectiveAuthRole(scenario.tags, authFeatureTags);
251
+
252
+ const code = await this.generateScenario(
253
+ scenario,
254
+ !!feature.background,
255
+ feature.tags || []
246
256
  );
257
+ renderedScenarios.push({ code, authRole });
247
258
  }
248
259
 
260
+ // Group scenarios by auth role for nested test.describe blocks
261
+ // This ensures test.use({ storageState }) only applies to its group
262
+ const authGroupMap = new Map<string, string[]>();
263
+ const groupOrder: string[] = [];
264
+ for (const { code, authRole } of renderedScenarios) {
265
+ const key = authRole || '';
266
+ if (!authGroupMap.has(key)) {
267
+ authGroupMap.set(key, []);
268
+ groupOrder.push(key);
269
+ }
270
+ authGroupMap.get(key)!.push(code);
271
+ }
272
+
273
+ const authGroups = groupOrder.map(key => ({
274
+ authRole: key || undefined,
275
+ scenarios: authGroupMap.get(key)!,
276
+ }));
277
+
278
+ // Determine rendering strategy:
279
+ // - Single group: flat structure (test.use at describe level if auth)
280
+ // - Multiple groups: nested describes per auth role
281
+ const needsGrouping = authGroups.length > 1;
282
+ const scenarios = renderedScenarios.map(s => s.code);
283
+
284
+ // For single group, extract the auth role to put test.use at describe level
285
+ const singleAuthRole = !needsGrouping && authGroups.length === 1
286
+ ? authGroups[0].authRole
287
+ : undefined;
288
+
249
289
  // Use adapter to render the complete test file structure
250
290
  return this.adapter.renderTestFile({
251
291
  imports: '', // Not used in template as it's rendered separately
252
292
  featureName: feature.name,
253
293
  featureDescription: feature.description,
254
294
  background,
255
- scenarios,
295
+ scenarios: needsGrouping ? [] : scenarios,
296
+ authGroups: needsGrouping ? authGroups : undefined,
297
+ singleAuthRole,
256
298
  });
257
299
  }
258
300
 
@@ -419,32 +419,32 @@ export const assertionPatterns: StepPattern[] = [
419
419
  {
420
420
  name: 'should-contain-text',
421
421
  matcher: (step: ParsedStep) =>
422
- step.text.includes('should contain') && !!step.selectorRef && !!(step.value || step.dataRef),
422
+ (step.text.includes('should contain') || step.text.includes('contains')) && !!step.selectorRef && !!(step.value || step.dataRef),
423
423
  resolver: (step, context): StepTemplateData => {
424
424
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
425
- const expectedText = step.value || `\${${step.dataRef}}`;
425
+ const expectedText = step.value || context.dataResolver.resolveData(step.dataRef!, context.featureName);
426
426
  return {
427
427
  templateName: 'contain-text-assertion',
428
428
  data: { ...resolved, expectedText },
429
429
  comment: `Assert ${step.selectorRef} contains "${step.value || step.dataRef}"`,
430
430
  };
431
431
  },
432
- priority: 9,
432
+ priority: 12,
433
433
  },
434
434
  {
435
435
  name: 'should-have-text',
436
436
  matcher: (step: ParsedStep) =>
437
- step.text.includes('should have text') && !!step.selectorRef && !!(step.value || step.dataRef),
437
+ (step.text.includes('should have text') || step.text.includes('has text')) && !!step.selectorRef && !!(step.value || step.dataRef),
438
438
  resolver: (step, context): StepTemplateData => {
439
439
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
440
- const expectedText = step.value || `\${${step.dataRef}}`;
440
+ const expectedText = step.value || context.dataResolver.resolveData(step.dataRef!, context.featureName);
441
441
  return {
442
442
  templateName: 'have-text-assertion',
443
443
  data: { ...resolved, expectedText },
444
444
  comment: `Assert ${step.selectorRef} has text "${step.value || step.dataRef}"`,
445
445
  };
446
446
  },
447
- priority: 10,
447
+ priority: 12,
448
448
  },
449
449
  {
450
450
  name: 'is-empty',
@@ -591,4 +591,60 @@ export const assertionPatterns: StepPattern[] = [
591
591
  },
592
592
  priority: 8,
593
593
  },
594
+ {
595
+ name: 'is-loading',
596
+ matcher: (step: ParsedStep) =>
597
+ /\b(see|sees)\s+\[/.test(step.text) && /\bis loading\b/.test(step.text) && !!step.selectorRef,
598
+ resolver: (step, context): StepTemplateData => {
599
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
600
+ return {
601
+ templateName: 'loading-assertion',
602
+ data: { ...resolved, selectorRef: step.selectorRef },
603
+ comment: `Assert ${step.selectorRef} is loading`,
604
+ };
605
+ },
606
+ priority: 11,
607
+ },
608
+ {
609
+ name: 'is-selected',
610
+ matcher: (step: ParsedStep) =>
611
+ /\b(see|sees)\s+\[/.test(step.text) && /\bis selected\b/.test(step.text) && !!step.selectorRef,
612
+ resolver: (step, context): StepTemplateData => {
613
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
614
+ return {
615
+ templateName: 'selected-assertion',
616
+ data: { ...resolved, selectorRef: step.selectorRef },
617
+ comment: `Assert ${step.selectorRef} is selected`,
618
+ };
619
+ },
620
+ priority: 11,
621
+ },
622
+ {
623
+ name: 'is-sorted-ascending',
624
+ matcher: (step: ParsedStep) =>
625
+ /\b(see|sees)\s+\[/.test(step.text) && /\bsorted ascending\b/.test(step.text) && !!step.selectorRef,
626
+ resolver: (step, context): StepTemplateData => {
627
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
628
+ return {
629
+ templateName: 'sorted-assertion',
630
+ data: { ...resolved, selectorRef: step.selectorRef, sortDirection: 'ascending' },
631
+ comment: `Assert ${step.selectorRef} is sorted ascending`,
632
+ };
633
+ },
634
+ priority: 12,
635
+ },
636
+ {
637
+ name: 'is-sorted-descending',
638
+ matcher: (step: ParsedStep) =>
639
+ /\b(see|sees)\s+\[/.test(step.text) && /\bsorted descending\b/.test(step.text) && !!step.selectorRef,
640
+ resolver: (step, context): StepTemplateData => {
641
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
642
+ return {
643
+ templateName: 'sorted-assertion',
644
+ data: { ...resolved, selectorRef: step.selectorRef, sortDirection: 'descending' },
645
+ comment: `Assert ${step.selectorRef} is sorted descending`,
646
+ };
647
+ },
648
+ priority: 12,
649
+ },
594
650
  ];
@@ -70,7 +70,7 @@ export const formPatterns: StepPattern[] = [
70
70
  {
71
71
  name: 'check-checkbox',
72
72
  matcher: (step: ParsedStep) =>
73
- step.text.includes('checks') && !!step.selectorRef,
73
+ (step.text.includes('checks') || step.text.match(/\bcheck\b/)) && !!step.selectorRef,
74
74
  resolver: (step, context) => {
75
75
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
76
76
  return {
@@ -84,7 +84,7 @@ export const formPatterns: StepPattern[] = [
84
84
  {
85
85
  name: 'uncheck-checkbox',
86
86
  matcher: (step: ParsedStep) =>
87
- step.text.includes('unchecks') && !!step.selectorRef,
87
+ (step.text.includes('unchecks') || step.text.match(/\buncheck\b/)) && !!step.selectorRef,
88
88
  resolver: (step, context) => {
89
89
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
90
90
  return {
@@ -151,7 +151,7 @@ export const formPatterns: StepPattern[] = [
151
151
  {
152
152
  name: 'clear-input',
153
153
  matcher: (step: ParsedStep) =>
154
- step.text.includes('clears') && !!step.selectorRef,
154
+ (step.text.includes('clears') || step.text.match(/\bclear\b/)) && !!step.selectorRef,
155
155
  resolver: (step, context) => {
156
156
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
157
157
  return {
@@ -135,7 +135,7 @@ export const interactionPatterns: StepPattern[] = [
135
135
  {
136
136
  name: 'hover-element',
137
137
  matcher: (step: ParsedStep) =>
138
- step.text.includes('hovers') && !!step.selectorRef,
138
+ (step.text.includes('hovers') || step.text.match(/\bhover\b/)) && !!step.selectorRef,
139
139
  resolver: (step, context) => {
140
140
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
141
141
  return {
@@ -305,4 +305,96 @@ export const interactionPatterns: StepPattern[] = [
305
305
  },
306
306
  priority: 8,
307
307
  },
308
+ {
309
+ name: 'toggle-switch',
310
+ matcher: (step: ParsedStep) =>
311
+ (step.text.includes('toggles') || step.text.match(/\btoggle\b/)) && !!step.selectorRef,
312
+ resolver: (step, context) => {
313
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
314
+ return {
315
+ templateName: 'toggle-action',
316
+ data: { ...resolved, selectorRef: step.selectorRef },
317
+ comment: `Toggle ${step.selectorRef}`,
318
+ };
319
+ },
320
+ priority: 9,
321
+ },
322
+ {
323
+ name: 'drag-to',
324
+ matcher: (step: ParsedStep) =>
325
+ (step.text.includes('drags') || step.text.match(/\bdrag\b/)) &&
326
+ /\bto\s+\[/.test(step.text) && !!step.selectorRef,
327
+ resolver: (step, context) => {
328
+ // Extract both selector refs: [Source] ... to [Target]
329
+ const allRefs = step.text.match(/\[([^\]]+)\]/g);
330
+ if (!allRefs || allRefs.length < 2) {
331
+ return {
332
+ templateName: 'drag-action',
333
+ data: { strategy: 'text', value: step.selectorRef, targetLocator: 'page.locator(\'TODO\')' },
334
+ comment: `Drag ${step.selectorRef} (missing target)`,
335
+ };
336
+ }
337
+ const sourceRef = allRefs[0].slice(1, -1);
338
+ const targetRef = allRefs[1].slice(1, -1);
339
+
340
+ const sourceResolved = context.selectorResolver.resolveSelector(sourceRef, undefined, step.elementType, step.nth);
341
+ const targetResolved = context.selectorResolver.resolveSelector(targetRef, undefined, undefined, 0);
342
+
343
+ // Build target locator expression from resolved selector
344
+ let targetLocator: string;
345
+ switch (targetResolved.strategy) {
346
+ case 'role':
347
+ targetLocator = targetResolved.name
348
+ ? `page.getByRole('${targetResolved.role}', { name: '${targetResolved.name}' })`
349
+ : `page.getByRole('${targetResolved.role}')`;
350
+ break;
351
+ case 'testid':
352
+ targetLocator = `page.getByTestId('${targetResolved.value}')`;
353
+ break;
354
+ case 'text':
355
+ targetLocator = `page.getByText('${targetResolved.value}')`;
356
+ break;
357
+ case 'locator':
358
+ targetLocator = `page.locator('${targetResolved.value}')`;
359
+ break;
360
+ default:
361
+ targetLocator = `page.getByText('${targetRef}')`;
362
+ }
363
+
364
+ return {
365
+ templateName: 'drag-action',
366
+ data: { ...sourceResolved, targetLocator },
367
+ comment: `Drag ${sourceRef} to ${targetRef}`,
368
+ };
369
+ },
370
+ priority: 10,
371
+ },
372
+ {
373
+ name: 'expand-element',
374
+ matcher: (step: ParsedStep) =>
375
+ (step.text.includes('expands') || step.text.match(/\bexpand\b/)) && !!step.selectorRef,
376
+ resolver: (step, context) => {
377
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
378
+ return {
379
+ templateName: 'expand-action',
380
+ data: { ...resolved, selectorRef: step.selectorRef, direction: 'expand' },
381
+ comment: `Expand ${step.selectorRef}`,
382
+ };
383
+ },
384
+ priority: 9,
385
+ },
386
+ {
387
+ name: 'collapse-element',
388
+ matcher: (step: ParsedStep) =>
389
+ (step.text.includes('collapses') || step.text.match(/\bcollapse\b/)) && !!step.selectorRef,
390
+ resolver: (step, context) => {
391
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
392
+ return {
393
+ templateName: 'expand-action',
394
+ data: { ...resolved, selectorRef: step.selectorRef, direction: 'collapse' },
395
+ comment: `Collapse ${step.selectorRef}`,
396
+ };
397
+ },
398
+ priority: 9,
399
+ },
308
400
  ];
@@ -89,6 +89,7 @@ export class TemplateEngine {
89
89
  // Switch/case helpers for cleaner strategy routing
90
90
  Handlebars.registerHelper('switch', function(value: any, options: any) {
91
91
  this.switchValue = value;
92
+ this.switchMatched = false;
92
93
  this.switchDefaulted = false;
93
94
  return options.fn(this);
94
95
  });
@@ -238,6 +239,8 @@ export class TemplateEngine {
238
239
  featureDescription?: string;
239
240
  background?: string;
240
241
  scenarios: string[];
242
+ authGroups?: Array<{ authRole?: string; scenarios: string[] }>;
243
+ singleAuthRole?: string;
241
244
  }): string {
242
245
  return this.render('test-file', data);
243
246
  }
@@ -251,6 +254,7 @@ export class TemplateEngine {
251
254
  renderScenario(data: {
252
255
  scenarioName: string;
253
256
  steps: Array<{ comment?: string; code: string }>;
257
+ authRole?: string;
254
258
  }): string {
255
259
  return this.render('scenario', data);
256
260
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
+ import { execSync } from 'child_process';
8
9
 
9
10
  export class ProjectInitializer {
10
11
  private baseCwd: string;
@@ -12,7 +13,7 @@ export class ProjectInitializer {
12
13
  private createdItems: string[] = [];
13
14
  private skippedItems: string[] = [];
14
15
 
15
- constructor() {
16
+ constructor(private baseUrl: string) {
16
17
  this.baseCwd = process.cwd();
17
18
  this.cwd = this.baseCwd;
18
19
  }
@@ -37,6 +38,9 @@ export class ProjectInitializer {
37
38
  // Create directories
38
39
  this.createDirectories();
39
40
 
41
+ // Ensure package.json and install Playwright
42
+ await this.setupDependencies();
43
+
40
44
  // Create playwright config if doesn't exist
41
45
  this.createPlaywrightConfig();
42
46
 
@@ -99,7 +103,8 @@ export class ProjectInitializer {
99
103
  return;
100
104
  }
101
105
 
102
- const configContent = this.getPlaywrightConfigTemplate();
106
+ const configContent = this.getPlaywrightConfigTemplate()
107
+ .replace('https://example.com', this.baseUrl);
103
108
  fs.writeFileSync(configPath, configContent, 'utf-8');
104
109
  this.createdItems.push('playwright.config.ts');
105
110
  }
@@ -174,9 +179,6 @@ export class ProjectInitializer {
174
179
  });
175
180
  }
176
181
 
177
- // Check for Playwright installation
178
- this.checkDependencies();
179
-
180
182
  console.log('\nNext steps:');
181
183
  let stepIndex = 1;
182
184
  if (projectName) {
@@ -192,48 +194,54 @@ export class ProjectInitializer {
192
194
  * Create AI rules files for GitHub Copilot and Claude Code
193
195
  */
194
196
  private createAIRules(): void {
195
- const rulesContent = this.getAIRulesContent();
196
-
197
- // GitHub Copilot: .github/copilot-instructions.md
198
- const githubDir = path.join(this.cwd, '.github');
199
- const copilotPath = path.join(githubDir, 'copilot-instructions.md');
200
- if (!fs.existsSync(copilotPath)) {
201
- if (!fs.existsSync(githubDir)) {
202
- fs.mkdirSync(githubDir, { recursive: true });
203
- }
204
- fs.writeFileSync(copilotPath, rulesContent, 'utf-8');
205
- this.createdItems.push('.github/copilot-instructions.md');
206
- } else {
207
- this.skippedItems.push('.github/copilot-instructions.md');
208
- }
197
+ const aiTemplateDir = path.join(__dirname, 'templates', 'ai-instructions');
198
+
199
+ // File mapping: [templateFile, outputPath]
200
+ const fileMapping: [string, string][] = [
201
+ // Config
202
+ ['claude-config.md', 'CLAUDE.md'],
203
+ ['copilot-config.md', '.github/copilot-instructions.md'],
204
+
205
+ // Commands — Claude Code
206
+ ['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
207
+ ['claude-cmd-make-tc.md', '.claude/commands/sungen/make-tc.md'],
208
+ ['claude-cmd-make-test.md', '.claude/commands/sungen/make-test.md'],
209
+
210
+ // Commands — GitHub Copilot
211
+ ['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
212
+ ['copilot-cmd-make-tc.md', '.github/prompts/sungen-make-tc.prompt.md'],
213
+ ['copilot-cmd-make-test.md', '.github/prompts/sungen-make-test.prompt.md'],
214
+
215
+ // Skills — Claude Code
216
+ ['claude-skill-gherkin-syntax.md', '.claude/skills/sungen-gherkin-syntax/SKILL.md'],
217
+ ['claude-skill-selector-keys.md', '.claude/skills/sungen-selector-keys/SKILL.md'],
218
+ ['claude-skill-error-mapping.md', '.claude/skills/sungen-error-mapping/SKILL.md'],
219
+
220
+ // Skills — GitHub Copilot
221
+ ['copilot-skill-gherkin-syntax.md', '.github/prompts/sungen-gherkin-syntax.prompt.md'],
222
+ ['copilot-skill-selector-keys.md', '.github/prompts/sungen-selector-keys.prompt.md'],
223
+ ['copilot-skill-error-mapping.md', '.github/prompts/sungen-error-mapping.prompt.md'],
224
+ ];
209
225
 
210
- // Claude Code: CLAUDE.md
211
- const claudePath = path.join(this.cwd, 'CLAUDE.md');
212
- if (!fs.existsSync(claudePath)) {
213
- fs.writeFileSync(claudePath, rulesContent, 'utf-8');
214
- this.createdItems.push('CLAUDE.md');
215
- } else {
216
- this.skippedItems.push('CLAUDE.md');
217
- }
226
+ for (const [templateFile, outputRelPath] of fileMapping) {
227
+ const outputPath = path.join(this.cwd, outputRelPath);
218
228
 
219
- // Gherkin Dictionary: docs/gherkin-dictionary.md
220
- const docsDir = path.join(this.cwd, 'docs');
221
- const dictPath = path.join(docsDir, 'gherkin-dictionary.md');
222
- if (!fs.existsSync(dictPath)) {
223
- // Copy from sungen package if available, otherwise generate stub
224
- const packageDictPath = path.join(__dirname, '..', '..', 'docs', 'gherkin-dictionary.md');
225
- if (!fs.existsSync(docsDir)) {
226
- fs.mkdirSync(docsDir, { recursive: true });
229
+ if (fs.existsSync(outputPath)) {
230
+ this.skippedItems.push(outputRelPath);
231
+ continue;
227
232
  }
228
- if (fs.existsSync(packageDictPath)) {
229
- fs.copyFileSync(packageDictPath, dictPath);
230
- } else {
231
- fs.writeFileSync(dictPath, '# Sungen Gherkin Dictionary\n\nSee https://github.com/sun-asterisk/sungen for the full dictionary.\n', 'utf-8');
233
+
234
+ const outputDir = path.dirname(outputPath);
235
+ if (!fs.existsSync(outputDir)) {
236
+ fs.mkdirSync(outputDir, { recursive: true });
232
237
  }
233
- this.createdItems.push('docs/gherkin-dictionary.md');
234
- } else {
235
- this.skippedItems.push('docs/gherkin-dictionary.md');
238
+
239
+ const templatePath = path.join(aiTemplateDir, templateFile);
240
+ const content = fs.readFileSync(templatePath, 'utf-8');
241
+ fs.writeFileSync(outputPath, content, 'utf-8');
242
+ this.createdItems.push(outputRelPath);
236
243
  }
244
+
237
245
  }
238
246
 
239
247
  /**
@@ -244,30 +252,38 @@ export class ProjectInitializer {
244
252
  return fs.readFileSync(templatePath, 'utf-8');
245
253
  }
246
254
 
247
- /**
248
- * Get AI rules content (shared between Copilot and Claude)
249
- */
250
- private getAIRulesContent(): string {
251
- return this.readTemplate('ai-rules.md');
252
- }
253
-
254
255
  /**
255
256
  * Check for required dependencies
256
257
  */
257
- private checkDependencies(): void {
258
+ private async setupDependencies(): Promise<void> {
259
+ const packageJsonPath = path.join(this.cwd, 'package.json');
260
+ const execOpts = { cwd: this.cwd, stdio: 'inherit' as const };
261
+
262
+ // Initialize package.json if it doesn't exist
263
+ if (!fs.existsSync(packageJsonPath)) {
264
+ console.log('šŸ“¦ No package.json found. Initializing...\n');
265
+ execSync('npm init -y', execOpts);
266
+ this.createdItems.push('package.json');
267
+ }
268
+
269
+ // Check if @playwright/test is already installed
258
270
  try {
259
- const packageJsonPath = path.join(this.cwd, 'package.json');
260
271
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
261
272
  const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
262
-
263
- if (!deps['@playwright/test']) {
264
- console.log('\nāš ļø Playwright not detected. Install with:');
265
- console.log(' npm install -D @playwright/test');
266
- console.log(' npx playwright install');
273
+ if (deps['@playwright/test']) {
274
+ console.log('āœ“ @playwright/test already installed\n');
275
+ return;
267
276
  }
268
- } catch (error) {
269
- // Ignore if can't read package.json
277
+ } catch {
278
+ // package.json just created, proceed with install
270
279
  }
280
+
281
+ // Install Playwright
282
+ console.log('šŸ“¦ Installing @playwright/test...\n');
283
+ execSync('npm install -D @playwright/test', execOpts);
284
+
285
+ console.log('\nšŸŽ­ Installing Playwright browsers...\n');
286
+ execSync('npx playwright install', execOpts);
271
287
  }
272
288
 
273
289
  /**
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: add-screen
3
+ description: 'Add a new Sungen screen — scaffolds directories and delegates to make-tc for test case creation'
4
+ argument-hint: [screen-name] [url-path]
5
+ allowed-tools: Read, Grep, Bash, Glob
6
+ ---
7
+
8
+ You are adding a new Sungen screen for test generation.
9
+
10
+ ## Parameters
11
+
12
+ Parse from `$ARGUMENTS`:
13
+ - **screen** — screen name (e.g., `login`, `dashboard`, `settings`)
14
+ - **path** — URL path (e.g., `/login`, `/dashboard`, `/settings`)
15
+
16
+ If **screen** is missing, ask: "What is the screen name? (e.g., `login`, `dashboard`)"
17
+ If **path** is missing, ask: "What is the URL path? (e.g., `/login`, `/dashboard`)"
18
+
19
+ ## Steps
20
+
21
+ ### 1. Scaffold the screen
22
+
23
+ Run:
24
+ ```bash
25
+ sungen add --screen <screen> --path <path>
26
+ ```
27
+
28
+ ### 2. Create test cases
29
+
30
+ Ask the user: "Would you like to create test cases now?"
31
+
32
+ If yes, delegate to `/sungen:make-tc <screen>` to handle:
33
+ - Exploring the live page or analyzing static designs
34
+ - Gathering test viewpoints (UI/UX, Validation, Logic, Security)
35
+ - Generating the 3 files (feature, selectors, test-data)
36
+
37
+ ### 3. Confirm
38
+
39
+ Tell the user what was created and next steps:
40
+ - If the page requires authentication before exploring via browser, read `baseURL` from `playwright.config.ts` and tell the user to manually run: `sungen makeauth <role> --url <baseURL>` (e.g., `sungen makeauth admin --url http://localhost:3000`). **Do NOT execute this command yourself.**
41
+ - Edit the generated files as needed
42
+ - Run `sungen generate --screen <screen>` to compile to Playwright `.spec.ts`
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: make-tc
3
+ description: 'Create test cases for a Sungen screen — gathers viewpoints, explores live page or static designs, generates feature/selectors/test-data files'
4
+ argument-hint: [screen-name]
5
+ allowed-tools: Read, Grep, Bash, Glob
6
+ ---
7
+
8
+ ## Role
9
+
10
+ You are a **Senior QA Engineer** specialized in test case design. You structure test cases by viewpoint categories and translate UI into Gherkin test cases following the `sungen-gherkin-syntax` and `sungen-selector-keys` skills.
11
+
12
+ ## Parameters
13
+
14
+ Parse from `$ARGUMENTS`:
15
+ - **screen** — screen name (e.g., `login`, `dashboard`)
16
+
17
+ If missing, ask the user.
18
+
19
+ ## Steps
20
+
21
+ ### 1. Verify screen exists
22
+
23
+ Check `qa/screens/<screen>/` exists. If not, tell user to run `/sungen:add-screen` first.
24
+
25
+ ### 2. Determine source mode
26
+
27
+ Ask: "Do you have a **live page** or **static designs** (screenshots, Figma, images)?"
28
+
29
+ **Live page →** explore via browser (see CLAUDE.md for Playwright MCP rules). If auth needed, check `specs/.auth/<role>.json` exists — if not, read `baseURL` from `playwright.config.ts` and tell the user to manually run: `sungen makeauth <role> --url <baseURL>` (e.g., `sungen makeauth admin --url http://localhost:3000`). **Do NOT execute `sungen makeauth` yourself.** Wait for the user to confirm auth is ready before proceeding.
30
+
31
+ **Static designs →** ask user to provide screenshot paths, Figma exports, or UI descriptions. Note: selectors will be best-guess until live page is available.
32
+
33
+ Present discovered elements to user, then proceed.
34
+
35
+ ### 3. Gather test viewpoints
36
+
37
+ | VP Category | Description |
38
+ |---|---|
39
+ | **UI/UX** | Default screen appearance, static elements, default states |
40
+ | **Validation** | Field/form validation, error messages |
41
+ | **Logic** | Business logic, happy paths, redirects |
42
+ | **Security** | Auth guards, permission checks |
43
+
44
+ For each viewpoint, gather: **UI Target**, **Test Viewpoint**, **Expected Result**.
45
+
46
+ User can provide all at once as a table:
47
+ ```
48
+ | VP Category | UI Target | Test Viewpoint | Expected Result |
49
+ ```
50
+
51
+ ### 4. Generate files
52
+
53
+ Generate the 3 files following `sungen-gherkin-syntax` and `sungen-selector-keys` skill rules:
54
+ - `qa/screens/<screen>/features/<screen>.feature` — one Scenario per viewpoint
55
+ - `qa/screens/<screen>/selectors/<screen>.yaml`
56
+ - `qa/screens/<screen>/test-data/<screen>.yaml`
57
+
58
+ ### 5. Confirm
59
+
60
+ Show what was generated. Next: `sungen generate --screen <screen>`