@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.
Files changed (104) hide show
  1. package/dist/cli/commands/dashboard.d.ts +10 -0
  2. package/dist/cli/commands/dashboard.d.ts.map +1 -0
  3. package/dist/cli/commands/dashboard.js +171 -0
  4. package/dist/cli/commands/dashboard.js.map +1 -0
  5. package/dist/cli/index.js +4 -2
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/dashboard/history-store.d.ts +27 -0
  8. package/dist/dashboard/history-store.d.ts.map +1 -0
  9. package/dist/dashboard/history-store.js +112 -0
  10. package/dist/dashboard/history-store.js.map +1 -0
  11. package/dist/dashboard/html-renderer.d.ts +30 -0
  12. package/dist/dashboard/html-renderer.d.ts.map +1 -0
  13. package/dist/dashboard/html-renderer.js +111 -0
  14. package/dist/dashboard/html-renderer.js.map +1 -0
  15. package/dist/dashboard/snapshot-builder.d.ts +30 -0
  16. package/dist/dashboard/snapshot-builder.d.ts.map +1 -0
  17. package/dist/dashboard/snapshot-builder.js +263 -0
  18. package/dist/dashboard/snapshot-builder.js.map +1 -0
  19. package/dist/dashboard/templates/index.html +287 -0
  20. package/dist/dashboard/types.d.ts +122 -0
  21. package/dist/dashboard/types.d.ts.map +1 -0
  22. package/dist/dashboard/types.js +11 -0
  23. package/dist/dashboard/types.js.map +1 -0
  24. package/dist/exporters/json-exporter.d.ts +25 -0
  25. package/dist/exporters/json-exporter.d.ts.map +1 -0
  26. package/dist/exporters/json-exporter.js +135 -0
  27. package/dist/exporters/json-exporter.js.map +1 -0
  28. package/dist/exporters/playwright-report-parser.d.ts +2 -1
  29. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  30. package/dist/exporters/playwright-report-parser.js +12 -5
  31. package/dist/exporters/playwright-report-parser.js.map +1 -1
  32. package/dist/exporters/spec-parser.d.ts.map +1 -1
  33. package/dist/exporters/spec-parser.js +8 -3
  34. package/dist/exporters/spec-parser.js.map +1 -1
  35. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +15 -0
  36. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  37. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -0
  38. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  39. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  40. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +2 -1
  41. package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +20 -1
  42. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +84 -4
  43. package/dist/generators/test-generator/code-generator.d.ts +1 -0
  44. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  45. package/dist/generators/test-generator/code-generator.js +76 -6
  46. package/dist/generators/test-generator/code-generator.js.map +1 -1
  47. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
  48. package/dist/generators/test-generator/patterns/interaction-patterns.js +22 -3
  49. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
  50. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +1 -1
  51. package/dist/generators/test-generator/patterns/navigation-patterns.js +8 -3
  52. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +1 -1
  53. package/dist/generators/test-generator/template-engine.d.ts +13 -0
  54. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  55. package/dist/generators/test-generator/template-engine.js +1 -1
  56. package/dist/generators/test-generator/template-engine.js.map +1 -1
  57. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  58. package/dist/orchestrator/screen-manager.js +3 -1
  59. package/dist/orchestrator/screen-manager.js.map +1 -1
  60. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +70 -10
  61. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +23 -0
  62. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +70 -10
  63. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +23 -0
  64. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  65. package/dist/orchestrator/templates/playwright.config.js +9 -1
  66. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  67. package/dist/orchestrator/templates/playwright.config.ts +11 -1
  68. package/dist/orchestrator/templates/specs-base.d.ts +3 -4
  69. package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
  70. package/dist/orchestrator/templates/specs-base.js +53 -39
  71. package/dist/orchestrator/templates/specs-base.js.map +1 -1
  72. package/dist/orchestrator/templates/specs-base.ts +55 -45
  73. package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -1
  74. package/dist/orchestrator/templates/specs-test-data.js +43 -0
  75. package/dist/orchestrator/templates/specs-test-data.js.map +1 -1
  76. package/dist/orchestrator/templates/specs-test-data.ts +47 -0
  77. package/package.json +4 -3
  78. package/src/cli/commands/dashboard.ts +158 -0
  79. package/src/cli/index.ts +4 -2
  80. package/src/dashboard/history-store.ts +86 -0
  81. package/src/dashboard/html-renderer.ts +90 -0
  82. package/src/dashboard/snapshot-builder.ts +273 -0
  83. package/src/dashboard/templates/index.html +287 -0
  84. package/src/dashboard/types.ts +148 -0
  85. package/src/exporters/json-exporter.ts +162 -0
  86. package/src/exporters/playwright-report-parser.ts +12 -5
  87. package/src/exporters/spec-parser.ts +8 -3
  88. package/src/generators/test-generator/adapters/adapter-interface.ts +6 -1
  89. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  90. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +2 -1
  91. package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +20 -1
  92. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +84 -4
  93. package/src/generators/test-generator/code-generator.ts +88 -7
  94. package/src/generators/test-generator/patterns/interaction-patterns.ts +25 -3
  95. package/src/generators/test-generator/patterns/navigation-patterns.ts +8 -3
  96. package/src/generators/test-generator/template-engine.ts +5 -2
  97. package/src/orchestrator/screen-manager.ts +3 -1
  98. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +70 -10
  99. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +23 -0
  100. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +70 -10
  101. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +23 -0
  102. package/src/orchestrator/templates/playwright.config.ts +11 -1
  103. package/src/orchestrator/templates/specs-base.ts +55 -45
  104. 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
- // Generate imports using adapter
194
- const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath });
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
- background = await this.generateBeforeEach(feature.background);
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
- const path = step.featurePath || '/';
300
- const pathRegex = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
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: pathCode.replace(/^['`]|['`]$/g, '') },
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: pathCode.replace(/^['`]|['`]$/g, '') },
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 (base.ts fixture) |
155
- | `@cleanup:forms` | Auto-cleanup: clear form fields after each test (base.ts fixture) |
156
- | `@cleanup:scroll` | Auto-cleanup: scroll to top after each test (base.ts fixture) |
157
- | `@cleanup:storage` | Auto-cleanup: clear sessionStorage after each test (base.ts fixture) |
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 (base.ts fixture) |
155
- | `@cleanup:forms` | Auto-cleanup: clear form fields after each test (base.ts fixture) |
156
- | `@cleanup:scroll` | Auto-cleanup: scroll to top after each test (base.ts fixture) |
157
- | `@cleanup:storage` | Auto-cleanup: clear sessionStorage after each test (base.ts fixture) |
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
- ['json', { outputFile: process.env.PLAYWRIGHT_JSON_OUTPUT_NAME || 'test-results/results.json' }],
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 */