@sun-asterisk/sungen 2.1.0 → 2.2.0

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 (173) hide show
  1. package/README.md +78 -51
  2. package/dist/cli/index.js +1 -1
  3. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +6 -1
  4. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  5. package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +0 -4
  6. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -0
  7. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +11 -0
  8. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +1 -1
  9. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +1 -1
  10. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/toggle-action.hbs +1 -0
  11. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +0 -1
  12. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +0 -1
  13. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +0 -1
  14. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/count-assertion.hbs +0 -1
  15. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +0 -1
  16. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +0 -1
  17. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +0 -1
  18. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +0 -1
  19. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +0 -1
  20. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +0 -1
  21. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/focused-assertion.hbs +0 -1
  22. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +0 -1
  23. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +0 -1
  24. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +0 -1
  25. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +0 -1
  26. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +0 -1
  27. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/is-hidden-assertion.hbs +0 -1
  28. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +0 -1
  29. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/list-item-count-assertion.hbs +0 -1
  30. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/loading-assertion.hbs +1 -0
  31. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +0 -1
  32. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +0 -1
  33. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +1 -0
  34. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/sorted-assertion.hbs +1 -0
  35. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +0 -1
  36. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +0 -1
  37. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-column-exists.hbs +0 -1
  38. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +0 -1
  39. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +0 -1
  40. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +0 -1
  41. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +0 -1
  42. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-assertion.hbs +0 -1
  43. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-dialog-heading-assertion.hbs +0 -1
  44. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +0 -1
  45. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +0 -1
  46. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +0 -1
  47. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +0 -1
  48. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +1 -1
  49. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +25 -0
  50. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  51. package/dist/generators/test-generator/code-generator.js +41 -3
  52. package/dist/generators/test-generator/code-generator.js.map +1 -1
  53. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
  54. package/dist/generators/test-generator/patterns/assertion-patterns.js +58 -6
  55. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
  56. package/dist/generators/test-generator/patterns/form-patterns.js +3 -3
  57. package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
  58. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
  59. package/dist/generators/test-generator/patterns/interaction-patterns.js +86 -1
  60. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
  61. package/dist/generators/test-generator/patterns/navigation-patterns.js +2 -2
  62. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +1 -1
  63. package/dist/generators/test-generator/template-engine.d.ts +6 -0
  64. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  65. package/dist/generators/test-generator/template-engine.js +1 -0
  66. package/dist/generators/test-generator/template-engine.js.map +1 -1
  67. package/dist/generators/test-generator/utils/selector-resolver.d.ts +15 -8
  68. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  69. package/dist/generators/test-generator/utils/selector-resolver.js +26 -197
  70. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  71. package/dist/orchestrator/project-initializer.d.ts +4 -0
  72. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  73. package/dist/orchestrator/project-initializer.js +49 -4
  74. package/dist/orchestrator/project-initializer.js.map +1 -1
  75. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +4 -3
  76. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -46
  77. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -46
  78. package/dist/orchestrator/templates/ai-instructions/claude-config.md +9 -8
  79. package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +11 -0
  80. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +8 -4
  81. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +206 -0
  82. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +19 -21
  83. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +256 -0
  84. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +14 -17
  85. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +16 -47
  86. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +16 -47
  87. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +8 -7
  88. package/dist/orchestrator/templates/ai-instructions/{copilot-skill-error-mapping.md → github-skill-sungen-error-mapping.md} +14 -3
  89. package/{src/orchestrator/templates/ai-instructions/copilot-skill-gherkin-syntax.md → dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md} +11 -7
  90. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +206 -0
  91. package/dist/orchestrator/templates/ai-instructions/{copilot-skill-selector-keys.md → github-skill-sungen-selector-keys.md} +22 -24
  92. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +256 -0
  93. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  94. package/dist/orchestrator/templates/playwright.config.js +3 -1
  95. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  96. package/dist/orchestrator/templates/playwright.config.ts +3 -1
  97. package/dist/orchestrator/templates/readme.md +78 -101
  98. package/package.json +1 -1
  99. package/src/cli/index.ts +1 -1
  100. package/src/generators/test-generator/adapters/adapter-interface.ts +7 -1
  101. package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +0 -4
  102. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -0
  103. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +11 -0
  104. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +1 -1
  105. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +1 -1
  106. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/toggle-action.hbs +1 -0
  107. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +0 -1
  108. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +0 -1
  109. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +0 -1
  110. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/count-assertion.hbs +0 -1
  111. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +0 -1
  112. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +0 -1
  113. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +0 -1
  114. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +0 -1
  115. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +0 -1
  116. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +0 -1
  117. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/focused-assertion.hbs +0 -1
  118. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +0 -1
  119. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +0 -1
  120. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +0 -1
  121. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +0 -1
  122. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +0 -1
  123. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/is-hidden-assertion.hbs +0 -1
  124. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +0 -1
  125. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/list-item-count-assertion.hbs +0 -1
  126. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/loading-assertion.hbs +1 -0
  127. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +0 -1
  128. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +0 -1
  129. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +1 -0
  130. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/sorted-assertion.hbs +1 -0
  131. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +0 -1
  132. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +0 -1
  133. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-column-exists.hbs +0 -1
  134. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +0 -1
  135. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +0 -1
  136. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +0 -1
  137. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +0 -1
  138. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-assertion.hbs +0 -1
  139. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-dialog-heading-assertion.hbs +0 -1
  140. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +0 -1
  141. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +0 -1
  142. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +0 -1
  143. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +0 -1
  144. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +1 -1
  145. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +25 -0
  146. package/src/generators/test-generator/code-generator.ts +50 -8
  147. package/src/generators/test-generator/patterns/assertion-patterns.ts +62 -6
  148. package/src/generators/test-generator/patterns/form-patterns.ts +3 -3
  149. package/src/generators/test-generator/patterns/interaction-patterns.ts +93 -1
  150. package/src/generators/test-generator/patterns/navigation-patterns.ts +2 -2
  151. package/src/generators/test-generator/template-engine.ts +4 -0
  152. package/src/generators/test-generator/utils/selector-resolver.ts +27 -204
  153. package/src/orchestrator/project-initializer.ts +60 -5
  154. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +4 -3
  155. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -46
  156. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -46
  157. package/src/orchestrator/templates/ai-instructions/claude-config.md +9 -8
  158. package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +11 -0
  159. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +8 -4
  160. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +206 -0
  161. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +19 -21
  162. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +256 -0
  163. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +14 -17
  164. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +16 -47
  165. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +16 -47
  166. package/src/orchestrator/templates/ai-instructions/copilot-config.md +8 -7
  167. package/src/orchestrator/templates/ai-instructions/{copilot-skill-error-mapping.md → github-skill-sungen-error-mapping.md} +14 -3
  168. package/{dist/orchestrator/templates/ai-instructions/copilot-skill-gherkin-syntax.md → src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md} +11 -7
  169. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +206 -0
  170. package/src/orchestrator/templates/ai-instructions/{copilot-skill-selector-keys.md → github-skill-sungen-selector-keys.md} +22 -24
  171. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +256 -0
  172. package/src/orchestrator/templates/playwright.config.ts +3 -1
  173. package/src/orchestrator/templates/readme.md +78 -101
@@ -419,32 +419,32 @@ export const assertionPatterns: StepPattern[] = [
419
419
  {
420
420
  name: 'should-contain-text',
421
421
  matcher: (step: ParsedStep) =>
422
- step.text.includes('should contain') && !!step.selectorRef && !!(step.value || step.dataRef),
422
+ (step.text.includes('should contain') || step.text.includes('contains')) && !!step.selectorRef && !!(step.value || step.dataRef),
423
423
  resolver: (step, context): StepTemplateData => {
424
424
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
425
- const expectedText = step.value || `\${${step.dataRef}}`;
425
+ const expectedText = step.value || context.dataResolver.resolveData(step.dataRef!, context.featureName);
426
426
  return {
427
427
  templateName: 'contain-text-assertion',
428
428
  data: { ...resolved, expectedText },
429
429
  comment: `Assert ${step.selectorRef} contains "${step.value || step.dataRef}"`,
430
430
  };
431
431
  },
432
- priority: 9,
432
+ priority: 12,
433
433
  },
434
434
  {
435
435
  name: 'should-have-text',
436
436
  matcher: (step: ParsedStep) =>
437
- step.text.includes('should have text') && !!step.selectorRef && !!(step.value || step.dataRef),
437
+ (step.text.includes('should have text') || step.text.includes('has text')) && !!step.selectorRef && !!(step.value || step.dataRef),
438
438
  resolver: (step, context): StepTemplateData => {
439
439
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
440
- const expectedText = step.value || `\${${step.dataRef}}`;
440
+ const expectedText = step.value || context.dataResolver.resolveData(step.dataRef!, context.featureName);
441
441
  return {
442
442
  templateName: 'have-text-assertion',
443
443
  data: { ...resolved, expectedText },
444
444
  comment: `Assert ${step.selectorRef} has text "${step.value || step.dataRef}"`,
445
445
  };
446
446
  },
447
- priority: 10,
447
+ priority: 12,
448
448
  },
449
449
  {
450
450
  name: 'is-empty',
@@ -591,4 +591,60 @@ export const assertionPatterns: StepPattern[] = [
591
591
  },
592
592
  priority: 8,
593
593
  },
594
+ {
595
+ name: 'is-loading',
596
+ matcher: (step: ParsedStep) =>
597
+ /\b(see|sees)\s+\[/.test(step.text) && /\bis loading\b/.test(step.text) && !!step.selectorRef,
598
+ resolver: (step, context): StepTemplateData => {
599
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
600
+ return {
601
+ templateName: 'loading-assertion',
602
+ data: { ...resolved, selectorRef: step.selectorRef },
603
+ comment: `Assert ${step.selectorRef} is loading`,
604
+ };
605
+ },
606
+ priority: 11,
607
+ },
608
+ {
609
+ name: 'is-selected',
610
+ matcher: (step: ParsedStep) =>
611
+ /\b(see|sees)\s+\[/.test(step.text) && /\bis selected\b/.test(step.text) && !!step.selectorRef,
612
+ resolver: (step, context): StepTemplateData => {
613
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
614
+ return {
615
+ templateName: 'selected-assertion',
616
+ data: { ...resolved, selectorRef: step.selectorRef },
617
+ comment: `Assert ${step.selectorRef} is selected`,
618
+ };
619
+ },
620
+ priority: 11,
621
+ },
622
+ {
623
+ name: 'is-sorted-ascending',
624
+ matcher: (step: ParsedStep) =>
625
+ /\b(see|sees)\s+\[/.test(step.text) && /\bsorted ascending\b/.test(step.text) && !!step.selectorRef,
626
+ resolver: (step, context): StepTemplateData => {
627
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
628
+ return {
629
+ templateName: 'sorted-assertion',
630
+ data: { ...resolved, selectorRef: step.selectorRef, sortDirection: 'ascending' },
631
+ comment: `Assert ${step.selectorRef} is sorted ascending`,
632
+ };
633
+ },
634
+ priority: 12,
635
+ },
636
+ {
637
+ name: 'is-sorted-descending',
638
+ matcher: (step: ParsedStep) =>
639
+ /\b(see|sees)\s+\[/.test(step.text) && /\bsorted descending\b/.test(step.text) && !!step.selectorRef,
640
+ resolver: (step, context): StepTemplateData => {
641
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
642
+ return {
643
+ templateName: 'sorted-assertion',
644
+ data: { ...resolved, selectorRef: step.selectorRef, sortDirection: 'descending' },
645
+ comment: `Assert ${step.selectorRef} is sorted descending`,
646
+ };
647
+ },
648
+ priority: 12,
649
+ },
594
650
  ];
@@ -70,7 +70,7 @@ export const formPatterns: StepPattern[] = [
70
70
  {
71
71
  name: 'check-checkbox',
72
72
  matcher: (step: ParsedStep) =>
73
- step.text.includes('checks') && !!step.selectorRef,
73
+ (step.text.includes('checks') || step.text.match(/\bcheck\b/)) && !!step.selectorRef,
74
74
  resolver: (step, context) => {
75
75
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
76
76
  return {
@@ -84,7 +84,7 @@ export const formPatterns: StepPattern[] = [
84
84
  {
85
85
  name: 'uncheck-checkbox',
86
86
  matcher: (step: ParsedStep) =>
87
- step.text.includes('unchecks') && !!step.selectorRef,
87
+ (step.text.includes('unchecks') || step.text.match(/\buncheck\b/)) && !!step.selectorRef,
88
88
  resolver: (step, context) => {
89
89
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
90
90
  return {
@@ -151,7 +151,7 @@ export const formPatterns: StepPattern[] = [
151
151
  {
152
152
  name: 'clear-input',
153
153
  matcher: (step: ParsedStep) =>
154
- step.text.includes('clears') && !!step.selectorRef,
154
+ (step.text.includes('clears') || step.text.match(/\bclear\b/)) && !!step.selectorRef,
155
155
  resolver: (step, context) => {
156
156
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
157
157
  return {
@@ -135,7 +135,7 @@ export const interactionPatterns: StepPattern[] = [
135
135
  {
136
136
  name: 'hover-element',
137
137
  matcher: (step: ParsedStep) =>
138
- step.text.includes('hovers') && !!step.selectorRef,
138
+ (step.text.includes('hovers') || step.text.match(/\bhover\b/)) && !!step.selectorRef,
139
139
  resolver: (step, context) => {
140
140
  const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
141
141
  return {
@@ -305,4 +305,96 @@ export const interactionPatterns: StepPattern[] = [
305
305
  },
306
306
  priority: 8,
307
307
  },
308
+ {
309
+ name: 'toggle-switch',
310
+ matcher: (step: ParsedStep) =>
311
+ (step.text.includes('toggles') || step.text.match(/\btoggle\b/)) && !!step.selectorRef,
312
+ resolver: (step, context) => {
313
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
314
+ return {
315
+ templateName: 'toggle-action',
316
+ data: { ...resolved, selectorRef: step.selectorRef },
317
+ comment: `Toggle ${step.selectorRef}`,
318
+ };
319
+ },
320
+ priority: 9,
321
+ },
322
+ {
323
+ name: 'drag-to',
324
+ matcher: (step: ParsedStep) =>
325
+ (step.text.includes('drags') || step.text.match(/\bdrag\b/)) &&
326
+ /\bto\s+\[/.test(step.text) && !!step.selectorRef,
327
+ resolver: (step, context) => {
328
+ // Extract both selector refs: [Source] ... to [Target]
329
+ const allRefs = step.text.match(/\[([^\]]+)\]/g);
330
+ if (!allRefs || allRefs.length < 2) {
331
+ return {
332
+ templateName: 'drag-action',
333
+ data: { strategy: 'text', value: step.selectorRef, targetLocator: 'page.locator(\'TODO\')' },
334
+ comment: `Drag ${step.selectorRef} (missing target)`,
335
+ };
336
+ }
337
+ const sourceRef = allRefs[0].slice(1, -1);
338
+ const targetRef = allRefs[1].slice(1, -1);
339
+
340
+ const sourceResolved = context.selectorResolver.resolveSelector(sourceRef, undefined, step.elementType, step.nth);
341
+ const targetResolved = context.selectorResolver.resolveSelector(targetRef, undefined, undefined, 0);
342
+
343
+ // Build target locator expression from resolved selector
344
+ let targetLocator: string;
345
+ switch (targetResolved.strategy) {
346
+ case 'role':
347
+ targetLocator = targetResolved.name
348
+ ? `page.getByRole('${targetResolved.role}', { name: '${targetResolved.name}' })`
349
+ : `page.getByRole('${targetResolved.role}')`;
350
+ break;
351
+ case 'testid':
352
+ targetLocator = `page.getByTestId('${targetResolved.value}')`;
353
+ break;
354
+ case 'text':
355
+ targetLocator = `page.getByText('${targetResolved.value}')`;
356
+ break;
357
+ case 'locator':
358
+ targetLocator = `page.locator('${targetResolved.value}')`;
359
+ break;
360
+ default:
361
+ targetLocator = `page.getByText('${targetRef}')`;
362
+ }
363
+
364
+ return {
365
+ templateName: 'drag-action',
366
+ data: { ...sourceResolved, targetLocator },
367
+ comment: `Drag ${sourceRef} to ${targetRef}`,
368
+ };
369
+ },
370
+ priority: 10,
371
+ },
372
+ {
373
+ name: 'expand-element',
374
+ matcher: (step: ParsedStep) =>
375
+ (step.text.includes('expands') || step.text.match(/\bexpand\b/)) && !!step.selectorRef,
376
+ resolver: (step, context) => {
377
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
378
+ return {
379
+ templateName: 'expand-action',
380
+ data: { ...resolved, selectorRef: step.selectorRef, direction: 'expand' },
381
+ comment: `Expand ${step.selectorRef}`,
382
+ };
383
+ },
384
+ priority: 9,
385
+ },
386
+ {
387
+ name: 'collapse-element',
388
+ matcher: (step: ParsedStep) =>
389
+ (step.text.includes('collapses') || step.text.match(/\bcollapse\b/)) && !!step.selectorRef,
390
+ resolver: (step, context) => {
391
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
392
+ return {
393
+ templateName: 'expand-action',
394
+ data: { ...resolved, selectorRef: step.selectorRef, direction: 'collapse' },
395
+ comment: `Collapse ${step.selectorRef}`,
396
+ };
397
+ },
398
+ priority: 9,
399
+ },
308
400
  ];
@@ -9,7 +9,7 @@ export const navigationPatterns: StepPattern[] = [
9
9
  {
10
10
  name: 'open-page-type',
11
11
  matcher: (step: ParsedStep) =>
12
- (step.text.includes('open') || step.text.includes('opens') || step.text.includes('is on')) &&
12
+ (step.text.includes('open') || step.text.includes('opens') || step.text.includes('is on') || step.text.includes('navigate to')) &&
13
13
  step.elementType === 'page',
14
14
  resolver: (step, context) => {
15
15
  let path = context.featurePath || '/';
@@ -64,7 +64,7 @@ export const navigationPatterns: StepPattern[] = [
64
64
  {
65
65
  name: 'navigate-to-route',
66
66
  matcher: (step: ParsedStep) =>
67
- (step.text.includes('navigates to') || step.text.includes('is on')) &&
67
+ (step.text.includes('navigate to') || step.text.includes('navigates to') || step.text.includes('is on')) &&
68
68
  !!(step.selectorRef || step.dataRef),
69
69
  resolver: (step, context) => {
70
70
  const route = step.selectorRef || step.dataRef;
@@ -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
  }
@@ -3,191 +3,6 @@ import * as path from 'path';
3
3
  import yaml from 'yaml';
4
4
  import { readYaml, readYamlIfExists } from '../../../utils/yaml-io';
5
5
 
6
- /**
7
- * Remove Vietnamese diacritics (accents) from text
8
- * Example: "Thời gian" → "Thoi gian", "Địa điểm" → "Dia diem"
9
- */
10
- function removeVietnameseTones(text: string): string {
11
- const vietnameseTones: { [key: string]: string } = {
12
- 'à': 'a', 'á': 'a', 'ả': 'a', 'ã': 'a', 'ạ': 'a',
13
- 'ă': 'a', 'ằ': 'a', 'ắ': 'a', 'ẳ': 'a', 'ẵ': 'a', 'ặ': 'a',
14
- 'â': 'a', 'ầ': 'a', 'ấ': 'a', 'ẩ': 'a', 'ẫ': 'a', 'ậ': 'a',
15
- 'đ': 'd',
16
- 'è': 'e', 'é': 'e', 'ẻ': 'e', 'ẽ': 'e', 'ẹ': 'e',
17
- 'ê': 'e', 'ề': 'e', 'ế': 'e', 'ể': 'e', 'ễ': 'e', 'ệ': 'e',
18
- 'ì': 'i', 'í': 'i', 'ỉ': 'i', 'ĩ': 'i', 'ị': 'i',
19
- 'ò': 'o', 'ó': 'o', 'ỏ': 'o', 'õ': 'o', 'ọ': 'o',
20
- 'ô': 'o', 'ồ': 'o', 'ố': 'o', 'ổ': 'o', 'ỗ': 'o', 'ộ': 'o',
21
- 'ơ': 'o', 'ờ': 'o', 'ớ': 'o', 'ở': 'o', 'ỡ': 'o', 'ợ': 'o',
22
- 'ù': 'u', 'ú': 'u', 'ủ': 'u', 'ũ': 'u', 'ụ': 'u',
23
- 'ư': 'u', 'ừ': 'u', 'ứ': 'u', 'ử': 'u', 'ữ': 'u', 'ự': 'u',
24
- 'ỳ': 'y', 'ý': 'y', 'ỷ': 'y', 'ỹ': 'y', 'ỵ': 'y',
25
- // Uppercase
26
- 'À': 'A', 'Á': 'A', 'Ả': 'A', 'Ã': 'A', 'Ạ': 'A',
27
- 'Ă': 'A', 'Ằ': 'A', 'Ắ': 'A', 'Ẳ': 'A', 'Ẵ': 'A', 'Ặ': 'A',
28
- 'Â': 'A', 'Ầ': 'A', 'Ấ': 'A', 'Ẩ': 'A', 'Ẫ': 'A', 'Ậ': 'A',
29
- 'Đ': 'D',
30
- 'È': 'E', 'É': 'E', 'Ẻ': 'E', 'Ẽ': 'E', 'Ẹ': 'E',
31
- 'Ê': 'E', 'Ề': 'E', 'Ế': 'E', 'Ể': 'E', 'Ễ': 'E', 'Ệ': 'E',
32
- 'Ì': 'I', 'Í': 'I', 'Ỉ': 'I', 'Ĩ': 'I', 'Ị': 'I',
33
- 'Ò': 'O', 'Ó': 'O', 'Ỏ': 'O', 'Õ': 'O', 'Ọ': 'O',
34
- 'Ô': 'O', 'Ồ': 'O', 'Ố': 'O', 'Ổ': 'O', 'Ỗ': 'O', 'Ộ': 'O',
35
- 'Ơ': 'O', 'Ờ': 'O', 'Ớ': 'O', 'Ở': 'O', 'Ỡ': 'O', 'Ợ': 'O',
36
- 'Ù': 'U', 'Ú': 'U', 'Ủ': 'U', 'Ũ': 'U', 'Ụ': 'U',
37
- 'Ư': 'U', 'Ừ': 'U', 'Ứ': 'U', 'Ử': 'U', 'Ữ': 'U', 'Ự': 'U',
38
- 'Ỳ': 'Y', 'Ý': 'Y', 'Ỷ': 'Y', 'Ỹ': 'Y', 'Ỵ': 'Y',
39
- };
40
-
41
- return text.split('').map(char => vietnameseTones[char] || char).join('');
42
- }
43
-
44
- /**
45
- * Convert Japanese Hiragana and Katakana to Romaji
46
- * Example: "ひらがな" → "hiragana", "カタカナ" → "katakana"
47
- */
48
- function convertJapaneseToRomaji(text: string): string {
49
- // Hiragana to Romaji mapping
50
- const hiraganaMap: { [key: string]: string } = {
51
- // Basic vowels
52
- 'あ': 'a', 'い': 'i', 'う': 'u', 'え': 'e', 'お': 'o',
53
- // K-row
54
- 'か': 'ka', 'き': 'ki', 'く': 'ku', 'け': 'ke', 'こ': 'ko',
55
- 'きゃ': 'kya', 'きゅ': 'kyu', 'きょ': 'kyo',
56
- // G-row
57
- 'が': 'ga', 'ぎ': 'gi', 'ぐ': 'gu', 'げ': 'ge', 'ご': 'go',
58
- 'ぎゃ': 'gya', 'ぎゅ': 'gyu', 'ぎょ': 'gyo',
59
- // S-row
60
- 'さ': 'sa', 'し': 'shi', 'す': 'su', 'せ': 'se', 'そ': 'so',
61
- 'しゃ': 'sha', 'しゅ': 'shu', 'しょ': 'sho',
62
- // Z-row
63
- 'ざ': 'za', 'じ': 'ji', 'ず': 'zu', 'ぜ': 'ze', 'ぞ': 'zo',
64
- 'じゃ': 'ja', 'じゅ': 'ju', 'じょ': 'jo',
65
- // T-row
66
- 'た': 'ta', 'ち': 'chi', 'つ': 'tsu', 'て': 'te', 'と': 'to',
67
- 'ちゃ': 'cha', 'ちゅ': 'chu', 'ちょ': 'cho',
68
- // D-row
69
- 'だ': 'da', 'ぢ': 'ji', 'づ': 'zu', 'で': 'de', 'ど': 'do',
70
- // N-row
71
- 'な': 'na', 'に': 'ni', 'ぬ': 'nu', 'ね': 'ne', 'の': 'no',
72
- 'にゃ': 'nya', 'にゅ': 'nyu', 'にょ': 'nyo',
73
- // H-row
74
- 'は': 'ha', 'ひ': 'hi', 'ふ': 'fu', 'へ': 'he', 'ほ': 'ho',
75
- 'ひゃ': 'hya', 'ひゅ': 'hyu', 'ひょ': 'hyo',
76
- // B-row
77
- 'ば': 'ba', 'び': 'bi', 'ぶ': 'bu', 'べ': 'be', 'ぼ': 'bo',
78
- 'びゃ': 'bya', 'びゅ': 'byu', 'びょ': 'byo',
79
- // P-row
80
- 'ぱ': 'pa', 'ぴ': 'pi', 'ぷ': 'pu', 'ぺ': 'pe', 'ぽ': 'po',
81
- 'ぴゃ': 'pya', 'ぴゅ': 'pyu', 'ぴょ': 'pyo',
82
- // M-row
83
- 'ま': 'ma', 'み': 'mi', 'む': 'mu', 'め': 'me', 'も': 'mo',
84
- 'みゃ': 'mya', 'みゅ': 'myu', 'みょ': 'myo',
85
- // Y-row
86
- 'や': 'ya', 'ゆ': 'yu', 'よ': 'yo',
87
- // R-row
88
- 'ら': 'ra', 'り': 'ri', 'る': 'ru', 'れ': 're', 'ろ': 'ro',
89
- 'りゃ': 'rya', 'りゅ': 'ryu', 'りょ': 'ryo',
90
- // W-row
91
- 'わ': 'wa', 'ゐ': 'wi', 'ゑ': 'we', 'を': 'wo', 'ん': 'n',
92
- // Small tsu (doubles next consonant)
93
- 'っ': '',
94
- };
95
-
96
- // Katakana to Romaji mapping
97
- const katakanaMap: { [key: string]: string } = {
98
- // Basic vowels
99
- 'ア': 'a', 'イ': 'i', 'ウ': 'u', 'エ': 'e', 'オ': 'o',
100
- // K-row
101
- 'カ': 'ka', 'キ': 'ki', 'ク': 'ku', 'ケ': 'ke', 'コ': 'ko',
102
- 'キャ': 'kya', 'キュ': 'kyu', 'キョ': 'kyo',
103
- // G-row
104
- 'ガ': 'ga', 'ギ': 'gi', 'グ': 'gu', 'ゲ': 'ge', 'ゴ': 'go',
105
- 'ギャ': 'gya', 'ギュ': 'gyu', 'ギョ': 'gyo',
106
- // S-row
107
- 'サ': 'sa', 'シ': 'shi', 'ス': 'su', 'セ': 'se', 'ソ': 'so',
108
- 'シャ': 'sha', 'シュ': 'shu', 'ショ': 'sho',
109
- // Z-row
110
- 'ザ': 'za', 'ジ': 'ji', 'ズ': 'zu', 'ゼ': 'ze', 'ゾ': 'zo',
111
- 'ジャ': 'ja', 'ジュ': 'ju', 'ジョ': 'jo',
112
- // T-row
113
- 'タ': 'ta', 'チ': 'chi', 'ツ': 'tsu', 'テ': 'te', 'ト': 'to',
114
- 'チャ': 'cha', 'チュ': 'chu', 'チョ': 'cho',
115
- // D-row
116
- 'ダ': 'da', 'ヂ': 'ji', 'ヅ': 'zu', 'デ': 'de', 'ド': 'do',
117
- // N-row
118
- 'ナ': 'na', 'ニ': 'ni', 'ヌ': 'nu', 'ネ': 'ne', 'ノ': 'no',
119
- 'ニャ': 'nya', 'ニュ': 'nyu', 'ニョ': 'nyo',
120
- // H-row
121
- 'ハ': 'ha', 'ヒ': 'hi', 'フ': 'fu', 'ヘ': 'he', 'ホ': 'ho',
122
- 'ヒャ': 'hya', 'ヒュ': 'hyu', 'ヒョ': 'hyo',
123
- // B-row
124
- 'バ': 'ba', 'ビ': 'bi', 'ブ': 'bu', 'ベ': 'be', 'ボ': 'bo',
125
- 'ビャ': 'bya', 'ビュ': 'byu', 'ビョ': 'byo',
126
- // P-row
127
- 'パ': 'pa', 'ピ': 'pi', 'プ': 'pu', 'ペ': 'pe', 'ポ': 'po',
128
- 'ピャ': 'pya', 'ピュ': 'pyu', 'ピョ': 'pyo',
129
- // M-row
130
- 'マ': 'ma', 'ミ': 'mi', 'ム': 'mu', 'メ': 'me', 'モ': 'mo',
131
- 'ミャ': 'mya', 'ミュ': 'myu', 'ミョ': 'myo',
132
- // Y-row
133
- 'ヤ': 'ya', 'ユ': 'yu', 'ヨ': 'yo',
134
- // R-row
135
- 'ラ': 'ra', 'リ': 'ri', 'ル': 'ru', 'レ': 're', 'ロ': 'ro',
136
- 'リャ': 'rya', 'リュ': 'ryu', 'リョ': 'ryo',
137
- // W-row
138
- 'ワ': 'wa', 'ヰ': 'wi', 'ヱ': 'we', 'ヲ': 'wo', 'ン': 'n',
139
- // Small tsu (doubles next consonant)
140
- 'ッ': '',
141
- // Extended katakana for foreign words
142
- 'ヴ': 'vu',
143
- 'ファ': 'fa', 'フィ': 'fi', 'フェ': 'fe', 'フォ': 'fo',
144
- 'ウィ': 'wi', 'ウェ': 'we', 'ウォ': 'wo',
145
- 'ティ': 'ti', 'ディ': 'di',
146
- 'トゥ': 'tu', 'ドゥ': 'du',
147
- };
148
-
149
- const allMaps = { ...hiraganaMap, ...katakanaMap };
150
-
151
- let result = '';
152
- let i = 0;
153
-
154
- while (i < text.length) {
155
- // Try to match 2-character combinations first (like きゃ, シャ)
156
- if (i < text.length - 1) {
157
- const twoChar = text.substring(i, i + 2);
158
- if (allMaps[twoChar]) {
159
- result += allMaps[twoChar];
160
- i += 2;
161
- continue;
162
- }
163
- }
164
-
165
- // Then try single character
166
- const oneChar = text[i];
167
- if (allMaps[oneChar]) {
168
- // Handle small tsu (っ/ッ) - double next consonant
169
- if (oneChar === 'っ' || oneChar === 'ッ') {
170
- if (i < text.length - 1) {
171
- const nextChar = text[i + 1];
172
- const nextRomaji = allMaps[nextChar];
173
- if (nextRomaji && nextRomaji.length > 0) {
174
- result += nextRomaji[0]; // Add first letter of next romaji
175
- }
176
- }
177
- } else {
178
- result += allMaps[oneChar];
179
- }
180
- i++;
181
- } else {
182
- // Not Japanese character, keep as is
183
- result += oneChar;
184
- i++;
185
- }
186
- }
187
-
188
- return result;
189
- }
190
-
191
6
  import { SelectorType } from '../../../utils/selector-types';
192
7
 
193
8
  // Structured selector format v2
@@ -288,23 +103,31 @@ export class SelectorResolver {
288
103
  }
289
104
 
290
105
  /**
291
- * Generate selector key from natural language label
292
- * "Email Address" "email.address"
293
- * "Submit Button" → "submit.button"
294
- * "User's Profile" → "users.profile"
295
- * "Thời gian" → "thoi.gian"
296
- * "Địa điểm" → "dia.diem"
297
- * "ログイン" → "roguin"
298
- * "パスワード" → "pasuwaado"
106
+ * Generate selector key from natural language label using Unicode NFC normalization.
107
+ * Preserves all Unicode characters (Vietnamese, Japanese, etc.) as-is.
108
+ *
109
+ * "Email Address" → "email address"
110
+ * "Submit Button" → "submit button"
111
+ * "User's Profile" → "user's profile"
112
+ * "Giới thiệu" → "giới thiệu"
113
+ * "Thời gian" → "thời gian"
114
+ * "ログイン" → "ログイン"
115
+ * "書類一覧" → "書類一覧"
299
116
  */
300
117
  static generateKey(label: string): string {
301
- return convertJapaneseToRomaji(removeVietnameseTones(label)) // Convert Japanese and Vietnamese first
302
- .toLowerCase()
303
- .replace(/['\u2019]s/g, 's') // Apostrophe s to just s ("User's" → "users")
304
- .replace(/['\u2019]/g, '') // Remove remaining apostrophes
305
- .replace(/[^a-z0-9\s]/g, '') // Remove special chars except spaces
306
- .trim()
307
- .replace(/\s+/g, '.'); // Spaces to dots
118
+ return label.normalize('NFC').toLowerCase().trim().replace(/\s+/g, ' ');
119
+ }
120
+
121
+ /**
122
+ * Normalize all keys in a SelectorFile through generateKey()
123
+ * so that YAML keys are case-insensitive and Unicode-normalized on lookup.
124
+ */
125
+ private static normalizeKeys(file: SelectorFile): SelectorFile {
126
+ const normalized: SelectorFile = {};
127
+ for (const [key, value] of Object.entries(file)) {
128
+ normalized[SelectorResolver.generateKey(key)] = value;
129
+ }
130
+ return normalized;
308
131
  }
309
132
 
310
133
  /**
@@ -742,7 +565,7 @@ export class SelectorResolver {
742
565
  if (basePath) {
743
566
  const baseSelectors = readYamlIfExists<SelectorFile>(basePath);
744
567
  if (baseSelectors) {
745
- Object.assign(selectors, baseSelectors);
568
+ Object.assign(selectors, SelectorResolver.normalizeKeys(baseSelectors));
746
569
  }
747
570
  }
748
571
 
@@ -751,7 +574,7 @@ export class SelectorResolver {
751
574
  if (baseOverridePath) {
752
575
  const overrideSelectors = readYamlIfExists<SelectorFile>(baseOverridePath);
753
576
  if (overrideSelectors) {
754
- Object.assign(selectors, overrideSelectors);
577
+ Object.assign(selectors, SelectorResolver.normalizeKeys(overrideSelectors));
755
578
  }
756
579
  }
757
580
  }
@@ -769,7 +592,7 @@ export class SelectorResolver {
769
592
  } else {
770
593
  const featureSelectors = readYamlIfExists<SelectorFile>(featurePath);
771
594
  if (featureSelectors) {
772
- Object.assign(selectors, featureSelectors);
595
+ Object.assign(selectors, SelectorResolver.normalizeKeys(featureSelectors));
773
596
  }
774
597
  }
775
598
 
@@ -778,7 +601,7 @@ export class SelectorResolver {
778
601
  if (featureOverridePath) {
779
602
  const overrideSelectors = readYamlIfExists<SelectorFile>(featureOverridePath);
780
603
  if (overrideSelectors) {
781
- Object.assign(selectors, overrideSelectors);
604
+ Object.assign(selectors, SelectorResolver.normalizeKeys(overrideSelectors));
782
605
  }
783
606
  }
784
607
 
@@ -53,6 +53,9 @@ export class ProjectInitializer {
53
53
  // Create AI rules files
54
54
  this.createAIRules();
55
55
 
56
+ // Create VS Code settings for Copilot auto-attach
57
+ this.createVSCodeSettings();
58
+
56
59
  // Display summary
57
60
  this.displaySummary(normalizedProjectName);
58
61
  }
@@ -159,6 +162,54 @@ export class ProjectInitializer {
159
162
  this.createdItems.push('README.md');
160
163
  }
161
164
 
165
+ /**
166
+ * Create VS Code settings for Copilot auto-attach
167
+ */
168
+ private createVSCodeSettings(): void {
169
+ const settingsPath = path.join(this.cwd, '.vscode', 'settings.json');
170
+
171
+ if (fs.existsSync(settingsPath)) {
172
+ this.skippedItems.push('.vscode/settings.json');
173
+ return;
174
+ }
175
+
176
+ const settingsDir = path.dirname(settingsPath);
177
+ if (!fs.existsSync(settingsDir)) {
178
+ fs.mkdirSync(settingsDir, { recursive: true });
179
+ }
180
+
181
+ const settings = {
182
+ 'github.copilot.chat.agent.runTasks': true,
183
+ 'chat.tools.terminal.autoApprove': {
184
+ sungen: true,
185
+ 'npx playwright': true,
186
+ },
187
+ };
188
+
189
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
190
+ this.createdItems.push('.vscode/settings.json');
191
+
192
+ // Create MCP config for Playwright browser tools
193
+ const mcpPath = path.join(this.cwd, '.vscode', 'mcp.json');
194
+
195
+ if (fs.existsSync(mcpPath)) {
196
+ this.skippedItems.push('.vscode/mcp.json');
197
+ return;
198
+ }
199
+
200
+ const mcpConfig = {
201
+ servers: {
202
+ playwright: {
203
+ command: 'npx',
204
+ args: ['@playwright/mcp@latest'],
205
+ },
206
+ },
207
+ };
208
+
209
+ fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf-8');
210
+ this.createdItems.push('.vscode/mcp.json');
211
+ }
212
+
162
213
  /**
163
214
  * Display initialization summary
164
215
  */
@@ -216,11 +267,15 @@ export class ProjectInitializer {
216
267
  ['claude-skill-gherkin-syntax.md', '.claude/skills/sungen-gherkin-syntax/SKILL.md'],
217
268
  ['claude-skill-selector-keys.md', '.claude/skills/sungen-selector-keys/SKILL.md'],
218
269
  ['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'],
270
+ ['claude-skill-tc-generation.md', '.claude/skills/sungen-tc-generation/SKILL.md'],
271
+ ['claude-skill-selector-fix.md', '.claude/skills/sungen-selector-fix/SKILL.md'],
272
+
273
+ // Skills — GitHub Copilot (separate copies with Copilot-friendly descriptions)
274
+ ['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
275
+ ['github-skill-sungen-selector-keys.md', '.github/skills/sungen-selector-keys/SKILL.md'],
276
+ ['github-skill-sungen-error-mapping.md', '.github/skills/sungen-error-mapping/SKILL.md'],
277
+ ['github-skill-sungen-tc-generation.md', '.github/skills/sungen-tc-generation/SKILL.md'],
278
+ ['github-skill-sungen-selector-fix.md', '.github/skills/sungen-selector-fix/SKILL.md'],
224
279
  ];
225
280
 
226
281
  for (const [templateFile, outputRelPath] of fileMapping) {
@@ -2,7 +2,7 @@
2
2
  name: add-screen
3
3
  description: 'Add a new Sungen screen — scaffolds directories and delegates to make-tc for test case creation'
4
4
  argument-hint: [screen-name] [url-path]
5
- allowed-tools: Read, Grep, Bash, Glob
5
+ allowed-tools: Read, Grep, Bash, Glob, AskUserQuestion
6
6
  ---
7
7
 
8
8
  You are adding a new Sungen screen for test generation.
@@ -37,6 +37,7 @@ If yes, delegate to `/sungen:make-tc <screen>` to handle:
37
37
  ### 3. Confirm
38
38
 
39
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.**
40
+ - If the page requires authentication, the user will be asked to log in via the MCP browser during `/sungen:make-tc`
41
41
  - Edit the generated files as needed
42
- - Run `sungen generate --screen <screen>` to compile to Playwright `.spec.ts`
42
+ - Run `/sungen:make-tc <screen>` to create test cases
43
+ - Run `/sungen:make-test <screen>` to generate selectors, compile, and run tests