@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.
- package/README.md +2 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +3 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +6 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +0 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +11 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/toggle-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/loading-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/sorted-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +25 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +41 -3
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.js +58 -6
- package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/form-patterns.js +3 -3
- package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js +86 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +6 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -0
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +3 -6
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +62 -58
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +42 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +60 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +59 -0
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +90 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +27 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +127 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +94 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +41 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +59 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +58 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +90 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-skill-error-mapping.md +27 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-skill-gherkin-syntax.md +127 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-skill-selector-keys.md +94 -0
- package/dist/orchestrator/templates/readme.md +42 -38
- package/docs/gherkin standards/gherkin-core-standard.md +141 -90
- package/docs/gherkin standards/gherkin-core-standard.vi.md +264 -54
- package/package.json +2 -2
- package/src/cli/commands/init.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +7 -1
- package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +0 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +11 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/toggle-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/loading-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/sorted-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +25 -0
- package/src/generators/test-generator/code-generator.ts +50 -8
- package/src/generators/test-generator/patterns/assertion-patterns.ts +62 -6
- package/src/generators/test-generator/patterns/form-patterns.ts +3 -3
- package/src/generators/test-generator/patterns/interaction-patterns.ts +93 -1
- package/src/generators/test-generator/template-engine.ts +4 -0
- package/src/orchestrator/project-initializer.ts +74 -58
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +42 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +60 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +59 -0
- package/src/orchestrator/templates/ai-instructions/claude-config.md +90 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +27 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +127 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +94 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +41 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +59 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +58 -0
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +90 -0
- package/src/orchestrator/templates/ai-instructions/copilot-skill-error-mapping.md +27 -0
- package/src/orchestrator/templates/ai-instructions/copilot-skill-gherkin-syntax.md +127 -0
- package/src/orchestrator/templates/ai-instructions/copilot-skill-selector-keys.md +94 -0
- package/src/orchestrator/templates/readme.md +42 -38
- package/dist/orchestrator/templates/ai-rules.md +0 -189
- 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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 ||
|
|
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:
|
|
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 ||
|
|
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:
|
|
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
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
fs.
|
|
233
|
+
|
|
234
|
+
const outputDir = path.dirname(outputPath);
|
|
235
|
+
if (!fs.existsSync(outputDir)) {
|
|
236
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
232
237
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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
|
-
|
|
264
|
-
|
|
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
|
|
269
|
-
//
|
|
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>`
|