@sun-asterisk/sungen 2.6.0 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/dashboard.d.ts +10 -0
- package/dist/cli/commands/dashboard.d.ts.map +1 -0
- package/dist/cli/commands/dashboard.js +171 -0
- package/dist/cli/commands/dashboard.js.map +1 -0
- package/dist/cli/index.js +4 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/history-store.d.ts +27 -0
- package/dist/dashboard/history-store.d.ts.map +1 -0
- package/dist/dashboard/history-store.js +112 -0
- package/dist/dashboard/history-store.js.map +1 -0
- package/dist/dashboard/html-renderer.d.ts +30 -0
- package/dist/dashboard/html-renderer.d.ts.map +1 -0
- package/dist/dashboard/html-renderer.js +111 -0
- package/dist/dashboard/html-renderer.js.map +1 -0
- package/dist/dashboard/snapshot-builder.d.ts +30 -0
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -0
- package/dist/dashboard/snapshot-builder.js +263 -0
- package/dist/dashboard/snapshot-builder.js.map +1 -0
- package/dist/dashboard/templates/index.html +287 -0
- package/dist/dashboard/types.d.ts +122 -0
- package/dist/dashboard/types.d.ts.map +1 -0
- package/dist/dashboard/types.js +11 -0
- package/dist/dashboard/types.js.map +1 -0
- package/dist/exporters/json-exporter.d.ts +25 -0
- package/dist/exporters/json-exporter.d.ts.map +1 -0
- package/dist/exporters/json-exporter.js +135 -0
- package/dist/exporters/json-exporter.js.map +1 -0
- package/dist/exporters/playwright-report-parser.d.ts +2 -1
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +12 -5
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +8 -3
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +15 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +2 -1
- package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +20 -1
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +84 -4
- package/dist/generators/test-generator/code-generator.d.ts +1 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +76 -6
- package/dist/generators/test-generator/code-generator.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 +22 -3
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.js +8 -3
- package/dist/generators/test-generator/patterns/navigation-patterns.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +13 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +3 -1
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +70 -10
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +23 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +70 -10
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +23 -0
- package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
- package/dist/orchestrator/templates/playwright.config.js +9 -1
- package/dist/orchestrator/templates/playwright.config.js.map +1 -1
- package/dist/orchestrator/templates/playwright.config.ts +11 -1
- package/dist/orchestrator/templates/specs-base.d.ts +3 -4
- package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-base.js +53 -39
- package/dist/orchestrator/templates/specs-base.js.map +1 -1
- package/dist/orchestrator/templates/specs-base.ts +55 -45
- package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-test-data.js +43 -0
- package/dist/orchestrator/templates/specs-test-data.js.map +1 -1
- package/dist/orchestrator/templates/specs-test-data.ts +47 -0
- package/package.json +4 -3
- package/src/cli/commands/dashboard.ts +158 -0
- package/src/cli/index.ts +4 -2
- package/src/dashboard/history-store.ts +86 -0
- package/src/dashboard/html-renderer.ts +90 -0
- package/src/dashboard/snapshot-builder.ts +273 -0
- package/src/dashboard/templates/index.html +287 -0
- package/src/dashboard/types.ts +148 -0
- package/src/exporters/json-exporter.ts +162 -0
- package/src/exporters/playwright-report-parser.ts +12 -5
- package/src/exporters/spec-parser.ts +8 -3
- package/src/generators/test-generator/adapters/adapter-interface.ts +6 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +2 -1
- package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +20 -1
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +84 -4
- package/src/generators/test-generator/code-generator.ts +88 -7
- package/src/generators/test-generator/patterns/interaction-patterns.ts +25 -3
- package/src/generators/test-generator/patterns/navigation-patterns.ts +8 -3
- package/src/generators/test-generator/template-engine.ts +5 -2
- package/src/orchestrator/screen-manager.ts +3 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +70 -10
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +23 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +70 -10
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +23 -0
- package/src/orchestrator/templates/playwright.config.ts +11 -1
- package/src/orchestrator/templates/specs-base.ts +55 -45
- package/src/orchestrator/templates/specs-test-data.ts +47 -0
|
@@ -48,6 +48,44 @@ function extractCleanupConfig(tags: string[]): string | undefined {
|
|
|
48
48
|
return entries.map(key => `${key}: true`).join(', ');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Extract @cleanup:* tags into structured flags for serial mode afterEach
|
|
53
|
+
*/
|
|
54
|
+
function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean } | undefined {
|
|
55
|
+
const validKeys = ['overlay', 'forms', 'scroll', 'storage'] as const;
|
|
56
|
+
const flags: Record<string, boolean> = {};
|
|
57
|
+
let found = false;
|
|
58
|
+
for (const tag of tags) {
|
|
59
|
+
if (!tag.startsWith('@cleanup:')) continue;
|
|
60
|
+
const key = tag.replace('@cleanup:', '');
|
|
61
|
+
if (validKeys.includes(key as any)) {
|
|
62
|
+
flags[key] = true;
|
|
63
|
+
found = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return found ? flags : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract pass-through tags (non-functional) for Playwright { tag: [...] }.
|
|
71
|
+
* Any tag not recognized by sungen as functional → pass through.
|
|
72
|
+
*/
|
|
73
|
+
const FUNCTIONAL_TAG_PREFIXES = [
|
|
74
|
+
'@parallel', '@cleanup:', '@auth:', '@manual', '@no-auth',
|
|
75
|
+
'@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
|
|
76
|
+
'@flow',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string | undefined {
|
|
80
|
+
const allTags = [...featureTags, ...scenarioTags];
|
|
81
|
+
const passThrough = allTags.filter(tag =>
|
|
82
|
+
!FUNCTIONAL_TAG_PREFIXES.some(prefix => tag.startsWith(prefix))
|
|
83
|
+
);
|
|
84
|
+
const unique = [...new Set(passThrough)];
|
|
85
|
+
if (unique.length === 0) return undefined;
|
|
86
|
+
return unique.map(t => `'${t}'`).join(', ');
|
|
87
|
+
}
|
|
88
|
+
|
|
51
89
|
/**
|
|
52
90
|
* Check for @screenshot:on-failure tag
|
|
53
91
|
*/
|
|
@@ -190,8 +228,12 @@ export class CodeGenerator {
|
|
|
190
228
|
const depth = outputSubdir ? outputSubdir.split(path.sep).length : 0;
|
|
191
229
|
const basePath = depth > 0 ? Array(depth).fill('..').join('/') : '..';
|
|
192
230
|
|
|
193
|
-
//
|
|
194
|
-
const
|
|
231
|
+
// Serial + @cleanup tags → need cleanupPage import from base
|
|
232
|
+
const isParallelFeature = (feature.tags || []).includes('@parallel');
|
|
233
|
+
const hasCleanupTags = (feature.tags || []).some(t => t.startsWith('@cleanup:'));
|
|
234
|
+
const needsCleanupImport = !isParallelFeature && hasCleanupTags;
|
|
235
|
+
|
|
236
|
+
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport });
|
|
195
237
|
|
|
196
238
|
// Generate test code (async now to support AI mapping)
|
|
197
239
|
const testCode = await this.generateTestCode(feature);
|
|
@@ -340,10 +382,18 @@ export class CodeGenerator {
|
|
|
340
382
|
}
|
|
341
383
|
}
|
|
342
384
|
|
|
385
|
+
// Detect @parallel opt-out (default is serial)
|
|
386
|
+
const isParallel = (feature.tags || []).includes('@parallel');
|
|
387
|
+
|
|
343
388
|
// Generate background if exists
|
|
344
389
|
let background: string | undefined;
|
|
390
|
+
let backgroundSteps: Array<{ comment?: string; code: string }> | undefined;
|
|
345
391
|
if (feature.background) {
|
|
346
|
-
|
|
392
|
+
if (isParallel) {
|
|
393
|
+
background = await this.generateBeforeEach(feature.background);
|
|
394
|
+
} else {
|
|
395
|
+
backgroundSteps = await this.generateBackgroundSteps(feature.background);
|
|
396
|
+
}
|
|
347
397
|
}
|
|
348
398
|
|
|
349
399
|
// Generate hook blocks
|
|
@@ -387,7 +437,8 @@ export class CodeGenerator {
|
|
|
387
437
|
const code = await this.generateScenario(
|
|
388
438
|
scenario,
|
|
389
439
|
!!feature.background,
|
|
390
|
-
feature.tags || []
|
|
440
|
+
feature.tags || [],
|
|
441
|
+
isParallel
|
|
391
442
|
);
|
|
392
443
|
renderedScenarios.push({ code, authRole });
|
|
393
444
|
}
|
|
@@ -414,6 +465,14 @@ export class CodeGenerator {
|
|
|
414
465
|
// - Single group: flat structure (test.use at describe level if auth)
|
|
415
466
|
// - Multiple groups: nested describes per auth role
|
|
416
467
|
const needsGrouping = authGroups.length > 1;
|
|
468
|
+
|
|
469
|
+
if (needsGrouping && !isParallel) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`Feature "${feature.name}" has multiple auth groups but no @parallel tag.\n` +
|
|
472
|
+
`Serial mode uses a shared browser context — it cannot mix different auth roles.\n` +
|
|
473
|
+
`Fix: add @parallel tag to the feature.`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
417
476
|
const scenarios = renderedScenarios.map(s => s.code);
|
|
418
477
|
|
|
419
478
|
// For single group, extract the auth role to put test.use at describe level
|
|
@@ -423,6 +482,7 @@ export class CodeGenerator {
|
|
|
423
482
|
|
|
424
483
|
// Extract @cleanup:* tags for autoCleanup fixture config
|
|
425
484
|
const cleanupConfig = extractCleanupConfig(feature.tags || []);
|
|
485
|
+
const cleanup = extractCleanupFlags(feature.tags || []);
|
|
426
486
|
const screenshotOnFailure = hasScreenshotOnFailure(feature.tags || []);
|
|
427
487
|
|
|
428
488
|
// Use adapter to render the complete test file structure
|
|
@@ -439,12 +499,27 @@ export class CodeGenerator {
|
|
|
439
499
|
runtimeData: this.options.runtimeData,
|
|
440
500
|
screenName: isFlowFeature ? `flows/${effectiveScreenName}` : effectiveScreenName,
|
|
441
501
|
featureFileName: featureName,
|
|
502
|
+
isParallel,
|
|
503
|
+
cleanup,
|
|
504
|
+
backgroundSteps,
|
|
442
505
|
scenarios: needsGrouping ? [] : scenarios,
|
|
443
506
|
authGroups: needsGrouping ? authGroups : undefined,
|
|
444
507
|
singleAuthRole,
|
|
445
508
|
});
|
|
446
509
|
}
|
|
447
510
|
|
|
511
|
+
private async generateBackgroundSteps(background: ParsedScenario): Promise<Array<{ comment?: string; code: string }>> {
|
|
512
|
+
const steps: Array<{ comment?: string; code: string }> = [];
|
|
513
|
+
for (const step of background.steps) {
|
|
514
|
+
const mapped = await Promise.resolve(this.stepMapper.mapStep(step));
|
|
515
|
+
steps.push({
|
|
516
|
+
comment: mapped.comment,
|
|
517
|
+
code: this.indentCode(mapped.code, 4),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return steps;
|
|
521
|
+
}
|
|
522
|
+
|
|
448
523
|
private async generateBeforeEach(background: ParsedScenario): Promise<string> {
|
|
449
524
|
// Map all steps
|
|
450
525
|
const steps: Array<{ comment?: string; code: string }> = [];
|
|
@@ -484,9 +559,10 @@ export class CodeGenerator {
|
|
|
484
559
|
}
|
|
485
560
|
|
|
486
561
|
private async generateScenario(
|
|
487
|
-
scenario: ParsedScenario,
|
|
562
|
+
scenario: ParsedScenario,
|
|
488
563
|
hasBackground: boolean,
|
|
489
|
-
featureTags: string[] = []
|
|
564
|
+
featureTags: string[] = [],
|
|
565
|
+
isParallel: boolean = false
|
|
490
566
|
): Promise<string> {
|
|
491
567
|
// Resolve base steps and tags for @extend scenarios
|
|
492
568
|
let stepsToMap = scenario.steps;
|
|
@@ -545,11 +621,16 @@ export class CodeGenerator {
|
|
|
545
621
|
}
|
|
546
622
|
}
|
|
547
623
|
|
|
624
|
+
// Extract pass-through tags (feature + scenario, excluding functional tags)
|
|
625
|
+
const tags = extractPassThroughTags(scenario.tags, featureTags);
|
|
626
|
+
|
|
548
627
|
// Use adapter to render scenario
|
|
549
628
|
return this.adapter.renderScenario({
|
|
550
629
|
scenarioName: scenario.name,
|
|
551
630
|
steps,
|
|
552
|
-
authRole
|
|
631
|
+
authRole,
|
|
632
|
+
isParallel,
|
|
633
|
+
tags,
|
|
553
634
|
});
|
|
554
635
|
}
|
|
555
636
|
|
|
@@ -296,12 +296,34 @@ export const interactionPatterns: StepPattern[] = [
|
|
|
296
296
|
matcher: (step: ParsedStep) =>
|
|
297
297
|
(step.text.includes('wait for') || step.text.includes('waits for')) && step.elementType === 'page',
|
|
298
298
|
resolver: (step, context) => {
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
let path = step.featurePath || '/';
|
|
300
|
+
|
|
301
|
+
if (step.selectorRef) {
|
|
302
|
+
try {
|
|
303
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
304
|
+
step.selectorRef, context.featureName, step.elementType, step.nth
|
|
305
|
+
);
|
|
306
|
+
path = resolved.value || path;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
// fallback to featurePath
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const isAbsoluteUrl = /^https?:\/\//.test(path);
|
|
313
|
+
let pathRegex: string;
|
|
314
|
+
if (isAbsoluteUrl) {
|
|
315
|
+
const url = new URL(path);
|
|
316
|
+
const hostEscaped = url.hostname.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
317
|
+
const pathEscaped = url.pathname !== '/' ? url.pathname.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&') : '';
|
|
318
|
+
pathRegex = hostEscaped + pathEscaped;
|
|
319
|
+
} else {
|
|
320
|
+
pathRegex = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
321
|
+
}
|
|
322
|
+
|
|
301
323
|
return {
|
|
302
324
|
templateName: 'wait-for-page',
|
|
303
325
|
data: { pathRegex },
|
|
304
|
-
comment: `Wait for page`,
|
|
326
|
+
comment: step.selectorRef ? `Wait for ${step.selectorRef} page` : `Wait for page`,
|
|
305
327
|
};
|
|
306
328
|
},
|
|
307
329
|
priority: 9,
|
|
@@ -26,10 +26,11 @@ export const navigationPatterns: StepPattern[] = [
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const finalPath = resolvePathVariables(path, context.scenarioSteps || []);
|
|
29
|
+
const isAbsoluteUrl = /^https?:\/\//.test(finalPath);
|
|
29
30
|
|
|
30
31
|
return {
|
|
31
32
|
templateName: 'navigation',
|
|
32
|
-
data: { baseURL: context.baseURL, path: finalPath },
|
|
33
|
+
data: { baseURL: isAbsoluteUrl ? undefined : context.baseURL, path: finalPath },
|
|
33
34
|
comment: step.selectorRef ? `Open ${step.selectorRef} page` : `Navigate to page`,
|
|
34
35
|
};
|
|
35
36
|
},
|
|
@@ -52,10 +53,12 @@ export const navigationPatterns: StepPattern[] = [
|
|
|
52
53
|
});
|
|
53
54
|
const resolvedPath = resolvePathVariables(inferredPath, context.scenarioSteps || []);
|
|
54
55
|
const pathCode = getPathCode(resolvedPath);
|
|
56
|
+
const cleanPath = pathCode.replace(/^['`]|['`]$/g, '');
|
|
57
|
+
const isAbsoluteUrl = /^https?:\/\//.test(cleanPath);
|
|
55
58
|
|
|
56
59
|
return {
|
|
57
60
|
templateName: 'navigation',
|
|
58
|
-
data: { baseURL: context.baseURL, path:
|
|
61
|
+
data: { baseURL: isAbsoluteUrl ? undefined : context.baseURL, path: cleanPath },
|
|
59
62
|
comment: `Open ${pageName}`,
|
|
60
63
|
};
|
|
61
64
|
},
|
|
@@ -75,10 +78,12 @@ export const navigationPatterns: StepPattern[] = [
|
|
|
75
78
|
});
|
|
76
79
|
const resolvedPath = resolvePathVariables(inferredPath, context.scenarioSteps || []);
|
|
77
80
|
const pathCode = getPathCode(resolvedPath);
|
|
81
|
+
const cleanPath = pathCode.replace(/^['`]|['`]$/g, '');
|
|
82
|
+
const isAbsoluteUrl = /^https?:\/\//.test(cleanPath);
|
|
78
83
|
|
|
79
84
|
return {
|
|
80
85
|
templateName: 'navigation',
|
|
81
|
-
data: { baseURL: context.baseURL, path:
|
|
86
|
+
data: { baseURL: isAbsoluteUrl ? undefined : context.baseURL, path: cleanPath },
|
|
82
87
|
comment: `Navigate to ${route}`,
|
|
83
88
|
};
|
|
84
89
|
},
|
|
@@ -229,8 +229,8 @@ export class TemplateEngine {
|
|
|
229
229
|
this.baseContext = {};
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string }): string {
|
|
233
|
-
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..' });
|
|
232
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean }): string {
|
|
233
|
+
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport });
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
renderTestFile(data: {
|
|
@@ -246,6 +246,9 @@ export class TemplateEngine {
|
|
|
246
246
|
runtimeData?: boolean;
|
|
247
247
|
screenName?: string;
|
|
248
248
|
featureFileName?: string;
|
|
249
|
+
isParallel?: boolean;
|
|
250
|
+
cleanup?: { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean };
|
|
251
|
+
backgroundSteps?: Array<{ comment?: string; code: string }>;
|
|
249
252
|
scenarios: string[];
|
|
250
253
|
authGroups?: Array<{ authRole?: string; scenarios: string[] }>;
|
|
251
254
|
singleAuthRole?: string;
|
|
@@ -358,9 +358,11 @@ export class ScreenManager {
|
|
|
358
358
|
So that I can accomplish my tasks
|
|
359
359
|
Path: ${featurePath}
|
|
360
360
|
|
|
361
|
+
Background:
|
|
362
|
+
Given User is on [${screenName}] page
|
|
363
|
+
|
|
361
364
|
@high
|
|
362
365
|
Scenario: Sample scenario for ${options.name}
|
|
363
|
-
Given User is on [${screenName}] page
|
|
364
366
|
When User click [element] button
|
|
365
367
|
Then User see [result] text with {{success}}
|
|
366
368
|
`;
|
|
@@ -120,6 +120,32 @@ Most elements auto-infer from `[Label] type` → `getByRole(type, { name: 'Label
|
|
|
120
120
|
- Same label, nth occurrence → add `--N` suffix
|
|
121
121
|
- Target Name > 30 chars → shorten to 1–3 meaningful words
|
|
122
122
|
|
|
123
|
+
## Dynamic Variables (test-data YAML)
|
|
124
|
+
|
|
125
|
+
Use `{{$var}}` in test-data YAML for values that must be unique per test run. Resolved at **runtime** by `TestDataLoader` — the compiler passes them through unchanged.
|
|
126
|
+
|
|
127
|
+
| Variable | Example | Output |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `{{$timestamp}}` | `"User-{{$timestamp}}"` | `"User-1714000000"` |
|
|
130
|
+
| `{{$uuid}}` | `"{{$uuid}}"` | `"a1b2c3d4-..."` |
|
|
131
|
+
| `{{$random:min:max}}` | `"{{$random:1:100}}"` | `"42"` |
|
|
132
|
+
| `{{$date}}` | `"{{$date}}"` | `"2026-04-24"` |
|
|
133
|
+
| `{{$datetime}}` | `"{{$datetime}}"` | `"2026-04-24T10:30:00.000Z"` |
|
|
134
|
+
|
|
135
|
+
**Rules:**
|
|
136
|
+
- `$timestamp` and `$uuid` → same value across all keys in one `load()` call (stable within a test file)
|
|
137
|
+
- `$random` → unique per occurrence (each key gets a different random)
|
|
138
|
+
- Resolved once at load time → every `get()` returns the same resolved value
|
|
139
|
+
- Use for CRUD flows to avoid data collision between parallel runs
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
# test-data/crud-award.yaml
|
|
143
|
+
award:
|
|
144
|
+
name: "Award-{{$timestamp}}"
|
|
145
|
+
email: "test+{{$uuid}}@example.com"
|
|
146
|
+
score: "{{$random:1:100}}"
|
|
147
|
+
```
|
|
148
|
+
|
|
123
149
|
## Selectors (priority order)
|
|
124
150
|
|
|
125
151
|
| type | value | name | use |
|
|
@@ -138,27 +164,60 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
|
|
|
138
164
|
|
|
139
165
|
## Tags
|
|
140
166
|
|
|
167
|
+
### Functional tags (affect code generation)
|
|
168
|
+
|
|
141
169
|
| Tag | Effect |
|
|
142
170
|
|---|---|
|
|
143
|
-
| `@auto` | Standard scenario, ready for automation |
|
|
144
171
|
| `@manual` | Skip in generation |
|
|
145
|
-
| `@smoke` / `@regression` | Test suite grouping |
|
|
146
|
-
| `@critical` | Priority: system unusable if fails (login, auth, core CRUD) |
|
|
147
|
-
| `@high` | Priority: major feature broken (required validation, key business rules) |
|
|
148
|
-
| `@normal` | Priority: degraded experience (UI layout, secondary flows) |
|
|
149
|
-
| `@low` | Priority: minor/cosmetic (tooltips, hover states, empty states) |
|
|
150
172
|
| `@auth:role` | Use auth storage state for role |
|
|
151
173
|
| `@no-auth` | Disable inherited auth |
|
|
152
174
|
| `@steps:name` | Define reusable step block (base scenario) |
|
|
153
175
|
| `@extend:name` | Prepend Given→When from @steps block (skip Then) |
|
|
154
|
-
| `@cleanup:overlay` | Auto-cleanup: dismiss dialogs/overlays after each test (
|
|
155
|
-
| `@cleanup:forms` | Auto-cleanup: clear form fields after each test (
|
|
156
|
-
| `@cleanup:scroll` | Auto-cleanup: scroll to top after each test (
|
|
157
|
-
| `@cleanup:storage` | Auto-cleanup: clear sessionStorage after each test (
|
|
176
|
+
| `@cleanup:overlay` | Auto-cleanup: dismiss dialogs/overlays after each test (cleanupPage) |
|
|
177
|
+
| `@cleanup:forms` | Auto-cleanup: clear form fields after each test (cleanupPage) |
|
|
178
|
+
| `@cleanup:scroll` | Auto-cleanup: scroll to top after each test (cleanupPage) |
|
|
179
|
+
| `@cleanup:storage` | Auto-cleanup: clear sessionStorage after each test (cleanupPage) |
|
|
158
180
|
| `@screenshot:on-failure` | Auto-capture screenshot when test fails (base.ts fixture) |
|
|
181
|
+
| `@parallel` | Opt-out: fresh page per test instead of serial default (for independent scenarios) |
|
|
159
182
|
| `@beforeAll` | Hook: runs once before all tests → `test.beforeAll()` |
|
|
160
183
|
| `@afterEach` | Hook: runs after each test → `test.afterEach()` (custom cleanup) |
|
|
161
184
|
| `@afterAll` | Hook: runs once after all tests → `test.afterAll()` |
|
|
185
|
+
| `@flow` | Mark feature as E2E flow (cross-screen testing) |
|
|
186
|
+
|
|
187
|
+
### Pass-through tags (filter at runtime via Playwright --grep)
|
|
188
|
+
|
|
189
|
+
Any tag not listed above passes through to Playwright `{ tag: [...] }`. Feature-level tags inherit to all scenarios.
|
|
190
|
+
|
|
191
|
+
| Tag | Purpose |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `@smoke` | Quick sanity check — run after every deploy |
|
|
194
|
+
| `@regression` | Full test suite — run nightly or before release |
|
|
195
|
+
| `@critical` | Priority: system unusable if fails (login, auth, core CRUD) |
|
|
196
|
+
| `@high` | Priority: major feature broken (required validation, key business rules) |
|
|
197
|
+
| `@normal` | Priority: degraded experience (UI layout, secondary flows) |
|
|
198
|
+
| `@low` | Priority: minor/cosmetic (tooltips, hover states, empty states) |
|
|
199
|
+
| `@auto` | Standard scenario, ready for automation |
|
|
200
|
+
| Any custom | e.g., `@sprint-42`, `@team-payment` — any tag works |
|
|
201
|
+
|
|
202
|
+
**Run filtered:**
|
|
203
|
+
```bash
|
|
204
|
+
npx playwright test --grep "@smoke" # only smoke tests
|
|
205
|
+
npx playwright test --grep "@critical" # only critical priority
|
|
206
|
+
npx playwright test --grep "@smoke|@critical" # smoke OR critical
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Serial vs Parallel (test execution mode)
|
|
210
|
+
|
|
211
|
+
**Default: serial** — `test.describe.serial()` with shared page. Background runs once in `beforeAll`. Fail → skip remaining.
|
|
212
|
+
|
|
213
|
+
**`@parallel` opt-out** — `test.describe()` with fresh page per test. Background runs as `beforeEach`. Use when scenarios are truly independent (validation rules, permission tests).
|
|
214
|
+
|
|
215
|
+
| Mode | Generated | Page | Background | On fail |
|
|
216
|
+
|---|---|---|---|---|
|
|
217
|
+
| Serial (default) | `test.describe.serial()` | Shared | `beforeAll` (1 goto) | Skip remaining |
|
|
218
|
+
| `@parallel` | `test.describe()` | Fresh per test | `beforeEach` (N goto) | Continue |
|
|
219
|
+
|
|
220
|
+
**`@parallel` is required** when a feature has multiple auth groups (e.g., `@auth:user` + `@no-auth` scenarios). Serial mode uses one shared browser context and cannot mix auth roles. The compiler will error if `@parallel` is missing in this case.
|
|
162
221
|
|
|
163
222
|
### `@flow` tag (E2E cross-screen testing)
|
|
164
223
|
|
|
@@ -172,6 +231,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
|
|
|
172
231
|
| Selectors | `[Element]` → own YAML | `[Screen:Element]` → own YAML (namespaced) |
|
|
173
232
|
| Test data | `{{variable}}` | `{{phase.variable}}` (namespaced by phase) |
|
|
174
233
|
| Tag | `@auto` / `@smoke` etc. | `@flow` (required at feature level) |
|
|
234
|
+
| Multi-domain | N/A | Absolute URL in selector `path:` skips baseURL |
|
|
175
235
|
|
|
176
236
|
**Selector namespace format:** `[Screen:Element]` where colon separates screen prefix from element name. The YAML key is `"screen:element"` (quoted, lowercase).
|
|
177
237
|
|
|
@@ -161,6 +161,29 @@ Scenario: Reset filters to default
|
|
|
161
161
|
|
|
162
162
|
For one-time setup/teardown. Most screens don't need these.
|
|
163
163
|
|
|
164
|
+
### `@parallel` — when tests need independent browser state
|
|
165
|
+
|
|
166
|
+
Add `@parallel` at feature level when:
|
|
167
|
+
|
|
168
|
+
1. **Multiple auth groups** (required) — e.g., `@auth:user` + `@no-auth` scenarios. Serial mode uses one shared context and cannot mix auth roles. Compiler will error without this tag.
|
|
169
|
+
2. **Validation-heavy features** (recommended) — each scenario fills forms with different invalid data and needs a clean form. Serial shared page keeps previous test's input.
|
|
170
|
+
|
|
171
|
+
Serial (default) is best for: CRUD flows, sequential user journeys, UI checks on the same page.
|
|
172
|
+
|
|
173
|
+
```gherkin
|
|
174
|
+
@parallel @auth:user
|
|
175
|
+
@cleanup:forms
|
|
176
|
+
Feature: kudos Screen
|
|
177
|
+
|
|
178
|
+
@critical
|
|
179
|
+
Scenario: Send kudos
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
@critical @no-auth
|
|
183
|
+
Scenario: Unauthenticated user is redirected
|
|
184
|
+
...
|
|
185
|
+
```
|
|
186
|
+
|
|
164
187
|
## Output Format
|
|
165
188
|
|
|
166
189
|
**Feature file** — `qa/screens/<screen>/features/<screen>.feature`
|
|
@@ -120,6 +120,32 @@ Most elements auto-infer from `[Label] type` → `getByRole(type, { name: 'Label
|
|
|
120
120
|
- Same label, nth occurrence → add `--N` suffix
|
|
121
121
|
- Target Name > 30 chars → shorten to 1–3 meaningful words
|
|
122
122
|
|
|
123
|
+
## Dynamic Variables (test-data YAML)
|
|
124
|
+
|
|
125
|
+
Use `{{$var}}` in test-data YAML for values that must be unique per test run. Resolved at **runtime** by `TestDataLoader` — the compiler passes them through unchanged.
|
|
126
|
+
|
|
127
|
+
| Variable | Example | Output |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `{{$timestamp}}` | `"User-{{$timestamp}}"` | `"User-1714000000"` |
|
|
130
|
+
| `{{$uuid}}` | `"{{$uuid}}"` | `"a1b2c3d4-..."` |
|
|
131
|
+
| `{{$random:min:max}}` | `"{{$random:1:100}}"` | `"42"` |
|
|
132
|
+
| `{{$date}}` | `"{{$date}}"` | `"2026-04-24"` |
|
|
133
|
+
| `{{$datetime}}` | `"{{$datetime}}"` | `"2026-04-24T10:30:00.000Z"` |
|
|
134
|
+
|
|
135
|
+
**Rules:**
|
|
136
|
+
- `$timestamp` and `$uuid` → same value across all keys in one `load()` call (stable within a test file)
|
|
137
|
+
- `$random` → unique per occurrence (each key gets a different random)
|
|
138
|
+
- Resolved once at load time → every `get()` returns the same resolved value
|
|
139
|
+
- Use for CRUD flows to avoid data collision between parallel runs
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
# test-data/crud-award.yaml
|
|
143
|
+
award:
|
|
144
|
+
name: "Award-{{$timestamp}}"
|
|
145
|
+
email: "test+{{$uuid}}@example.com"
|
|
146
|
+
score: "{{$random:1:100}}"
|
|
147
|
+
```
|
|
148
|
+
|
|
123
149
|
## Selectors (priority order)
|
|
124
150
|
|
|
125
151
|
| type | value | name | use |
|
|
@@ -138,27 +164,60 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
|
|
|
138
164
|
|
|
139
165
|
## Tags
|
|
140
166
|
|
|
167
|
+
### Functional tags (affect code generation)
|
|
168
|
+
|
|
141
169
|
| Tag | Effect |
|
|
142
170
|
|---|---|
|
|
143
|
-
| `@auto` | Standard scenario, ready for automation |
|
|
144
171
|
| `@manual` | Skip in generation |
|
|
145
|
-
| `@smoke` / `@regression` | Test suite grouping |
|
|
146
|
-
| `@critical` | Priority: system unusable if fails (login, auth, core CRUD) |
|
|
147
|
-
| `@high` | Priority: major feature broken (required validation, key business rules) |
|
|
148
|
-
| `@normal` | Priority: degraded experience (UI layout, secondary flows) |
|
|
149
|
-
| `@low` | Priority: minor/cosmetic (tooltips, hover states, empty states) |
|
|
150
172
|
| `@auth:role` | Use auth storage state for role |
|
|
151
173
|
| `@no-auth` | Disable inherited auth |
|
|
152
174
|
| `@steps:name` | Define reusable step block (base scenario) |
|
|
153
175
|
| `@extend:name` | Prepend Given→When from @steps block (skip Then) |
|
|
154
|
-
| `@cleanup:overlay` | Auto-cleanup: dismiss dialogs/overlays after each test (
|
|
155
|
-
| `@cleanup:forms` | Auto-cleanup: clear form fields after each test (
|
|
156
|
-
| `@cleanup:scroll` | Auto-cleanup: scroll to top after each test (
|
|
157
|
-
| `@cleanup:storage` | Auto-cleanup: clear sessionStorage after each test (
|
|
176
|
+
| `@cleanup:overlay` | Auto-cleanup: dismiss dialogs/overlays after each test (cleanupPage) |
|
|
177
|
+
| `@cleanup:forms` | Auto-cleanup: clear form fields after each test (cleanupPage) |
|
|
178
|
+
| `@cleanup:scroll` | Auto-cleanup: scroll to top after each test (cleanupPage) |
|
|
179
|
+
| `@cleanup:storage` | Auto-cleanup: clear sessionStorage after each test (cleanupPage) |
|
|
158
180
|
| `@screenshot:on-failure` | Auto-capture screenshot when test fails (base.ts fixture) |
|
|
181
|
+
| `@parallel` | Opt-out: fresh page per test instead of serial default (for independent scenarios) |
|
|
159
182
|
| `@beforeAll` | Hook: runs once before all tests → `test.beforeAll()` |
|
|
160
183
|
| `@afterEach` | Hook: runs after each test → `test.afterEach()` (custom cleanup) |
|
|
161
184
|
| `@afterAll` | Hook: runs once after all tests → `test.afterAll()` |
|
|
185
|
+
| `@flow` | Mark feature as E2E flow (cross-screen testing) |
|
|
186
|
+
|
|
187
|
+
### Pass-through tags (filter at runtime via Playwright --grep)
|
|
188
|
+
|
|
189
|
+
Any tag not listed above passes through to Playwright `{ tag: [...] }`. Feature-level tags inherit to all scenarios.
|
|
190
|
+
|
|
191
|
+
| Tag | Purpose |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `@smoke` | Quick sanity check — run after every deploy |
|
|
194
|
+
| `@regression` | Full test suite — run nightly or before release |
|
|
195
|
+
| `@critical` | Priority: system unusable if fails (login, auth, core CRUD) |
|
|
196
|
+
| `@high` | Priority: major feature broken (required validation, key business rules) |
|
|
197
|
+
| `@normal` | Priority: degraded experience (UI layout, secondary flows) |
|
|
198
|
+
| `@low` | Priority: minor/cosmetic (tooltips, hover states, empty states) |
|
|
199
|
+
| `@auto` | Standard scenario, ready for automation |
|
|
200
|
+
| Any custom | e.g., `@sprint-42`, `@team-payment` — any tag works |
|
|
201
|
+
|
|
202
|
+
**Run filtered:**
|
|
203
|
+
```bash
|
|
204
|
+
npx playwright test --grep "@smoke" # only smoke tests
|
|
205
|
+
npx playwright test --grep "@critical" # only critical priority
|
|
206
|
+
npx playwright test --grep "@smoke|@critical" # smoke OR critical
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Serial vs Parallel (test execution mode)
|
|
210
|
+
|
|
211
|
+
**Default: serial** — `test.describe.serial()` with shared page. Background runs once in `beforeAll`. Fail → skip remaining.
|
|
212
|
+
|
|
213
|
+
**`@parallel` opt-out** — `test.describe()` with fresh page per test. Background runs as `beforeEach`. Use when scenarios are truly independent (validation rules, permission tests).
|
|
214
|
+
|
|
215
|
+
| Mode | Generated | Page | Background | On fail |
|
|
216
|
+
|---|---|---|---|---|
|
|
217
|
+
| Serial (default) | `test.describe.serial()` | Shared | `beforeAll` (1 goto) | Skip remaining |
|
|
218
|
+
| `@parallel` | `test.describe()` | Fresh per test | `beforeEach` (N goto) | Continue |
|
|
219
|
+
|
|
220
|
+
**`@parallel` is required** when a feature has multiple auth groups (e.g., `@auth:user` + `@no-auth` scenarios). Serial mode uses one shared browser context and cannot mix auth roles. The compiler will error if `@parallel` is missing in this case.
|
|
162
221
|
|
|
163
222
|
### `@flow` tag (E2E cross-screen testing)
|
|
164
223
|
|
|
@@ -172,6 +231,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
|
|
|
172
231
|
| Selectors | `[Element]` → own YAML | `[Screen:Element]` → own YAML (namespaced) |
|
|
173
232
|
| Test data | `{{variable}}` | `{{phase.variable}}` (namespaced by phase) |
|
|
174
233
|
| Tag | `@auto` / `@smoke` etc. | `@flow` (required at feature level) |
|
|
234
|
+
| Multi-domain | N/A | Absolute URL in selector `path:` skips baseURL |
|
|
175
235
|
|
|
176
236
|
**Selector namespace format:** `[Screen:Element]` where colon separates screen prefix from element name. The YAML key is `"screen:element"` (quoted, lowercase).
|
|
177
237
|
|
|
@@ -174,6 +174,29 @@ Scenario: Reset filters to default
|
|
|
174
174
|
|
|
175
175
|
For one-time setup/teardown. Most screens don't need these.
|
|
176
176
|
|
|
177
|
+
### `@parallel` — when tests need independent browser state
|
|
178
|
+
|
|
179
|
+
Add `@parallel` at feature level when:
|
|
180
|
+
|
|
181
|
+
1. **Multiple auth groups** (required) — e.g., `@auth:user` + `@no-auth` scenarios. Serial mode uses one shared context and cannot mix auth roles. Compiler will error without this tag.
|
|
182
|
+
2. **Validation-heavy features** (recommended) — each scenario fills forms with different invalid data and needs a clean form. Serial shared page keeps previous test's input.
|
|
183
|
+
|
|
184
|
+
Serial (default) is best for: CRUD flows, sequential user journeys, UI checks on the same page.
|
|
185
|
+
|
|
186
|
+
```gherkin
|
|
187
|
+
@parallel @auth:user
|
|
188
|
+
@cleanup:forms
|
|
189
|
+
Feature: kudos Screen
|
|
190
|
+
|
|
191
|
+
@critical
|
|
192
|
+
Scenario: Send kudos
|
|
193
|
+
...
|
|
194
|
+
|
|
195
|
+
@critical @no-auth
|
|
196
|
+
Scenario: Unauthenticated user is redirected
|
|
197
|
+
...
|
|
198
|
+
```
|
|
199
|
+
|
|
177
200
|
## Output Format
|
|
178
201
|
|
|
179
202
|
**Feature file** — `qa/screens/<screen>/features/<screen>.feature`
|
|
@@ -28,7 +28,14 @@ export default defineConfig({
|
|
|
28
28
|
/* Output file path is controlled by PLAYWRIGHT_JSON_OUTPUT_NAME env var for per-screen isolation. */
|
|
29
29
|
reporter: [
|
|
30
30
|
['html'],
|
|
31
|
-
[
|
|
31
|
+
[
|
|
32
|
+
'json',
|
|
33
|
+
{
|
|
34
|
+
outputFile:
|
|
35
|
+
process.env.PLAYWRIGHT_JSON_OUTPUT_NAME ||
|
|
36
|
+
'test-results/results.json',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
32
39
|
],
|
|
33
40
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
34
41
|
use: {
|
|
@@ -40,6 +47,9 @@ export default defineConfig({
|
|
|
40
47
|
|
|
41
48
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
42
49
|
trace: 'on-first-retry',
|
|
50
|
+
|
|
51
|
+
/* Capture screenshot after each test failure. */
|
|
52
|
+
screenshot: 'only-on-failure',
|
|
43
53
|
},
|
|
44
54
|
|
|
45
55
|
/* Configure projects for major browsers */
|