@sun-asterisk/sungen 3.2.21 → 3.2.22-beta.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 (188) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +8 -0
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/commands/delivery.d.ts +3 -0
  5. package/dist/cli/commands/delivery.d.ts.map +1 -1
  6. package/dist/cli/commands/delivery.js +15 -0
  7. package/dist/cli/commands/delivery.js.map +1 -1
  8. package/dist/cli/commands/inspect.d.ts +41 -0
  9. package/dist/cli/commands/inspect.d.ts.map +1 -0
  10. package/dist/cli/commands/inspect.js +134 -0
  11. package/dist/cli/commands/inspect.js.map +1 -0
  12. package/dist/cli/commands/trace.d.ts.map +1 -1
  13. package/dist/cli/commands/trace.js +9 -0
  14. package/dist/cli/commands/trace.js.map +1 -1
  15. package/dist/cli/index.js +2 -0
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/exporters/matrix/build.d.ts +10 -0
  18. package/dist/exporters/matrix/build.d.ts.map +1 -1
  19. package/dist/exporters/matrix/build.js +38 -0
  20. package/dist/exporters/matrix/build.js.map +1 -1
  21. package/dist/exporters/matrix/export.d.ts.map +1 -1
  22. package/dist/exporters/matrix/export.js +11 -0
  23. package/dist/exporters/matrix/export.js.map +1 -1
  24. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  25. package/dist/exporters/matrix/render-xlsx.js +44 -1
  26. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  27. package/dist/exporters/matrix/types.d.ts +10 -0
  28. package/dist/exporters/matrix/types.d.ts.map +1 -1
  29. package/dist/exporters/matrix/types.js.map +1 -1
  30. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  31. package/dist/exporters/playwright-report-parser.js +1 -0
  32. package/dist/exporters/playwright-report-parser.js.map +1 -1
  33. package/dist/exporters/types.d.ts +2 -0
  34. package/dist/exporters/types.d.ts.map +1 -1
  35. package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
  36. package/dist/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
  37. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
  38. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
  39. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
  40. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
  41. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
  42. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  43. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
  44. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
  45. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
  46. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
  47. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
  48. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
  49. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
  50. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
  51. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
  52. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
  53. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
  54. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
  55. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
  56. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
  57. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
  58. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
  59. package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
  60. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
  61. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
  62. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
  63. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
  64. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  65. package/dist/generators/test-generator/code-generator.js +29 -8
  66. package/dist/generators/test-generator/code-generator.js.map +1 -1
  67. package/dist/generators/test-generator/diagnostics.d.ts +25 -1
  68. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  69. package/dist/generators/test-generator/diagnostics.js +24 -0
  70. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  71. package/dist/generators/test-generator/patterns/index.d.ts +45 -0
  72. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  73. package/dist/generators/test-generator/patterns/index.js +159 -20
  74. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  75. package/dist/generators/test-generator/patterns/types.d.ts +36 -0
  76. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  77. package/dist/generators/test-generator/step-mapper.d.ts +33 -0
  78. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  79. package/dist/generators/test-generator/step-mapper.js +100 -24
  80. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  81. package/dist/harness/audit.d.ts +2 -0
  82. package/dist/harness/audit.d.ts.map +1 -1
  83. package/dist/harness/audit.js +101 -10
  84. package/dist/harness/audit.js.map +1 -1
  85. package/dist/harness/flow-contract.d.ts +87 -0
  86. package/dist/harness/flow-contract.d.ts.map +1 -0
  87. package/dist/harness/flow-contract.js +259 -0
  88. package/dist/harness/flow-contract.js.map +1 -0
  89. package/dist/harness/flow-plan.d.ts +3 -0
  90. package/dist/harness/flow-plan.d.ts.map +1 -1
  91. package/dist/harness/flow-plan.js +6 -2
  92. package/dist/harness/flow-plan.js.map +1 -1
  93. package/dist/harness/parse.d.ts +5 -0
  94. package/dist/harness/parse.d.ts.map +1 -1
  95. package/dist/harness/parse.js +29 -1
  96. package/dist/harness/parse.js.map +1 -1
  97. package/dist/harness/perf.d.ts +40 -0
  98. package/dist/harness/perf.d.ts.map +1 -0
  99. package/dist/harness/perf.js +136 -0
  100. package/dist/harness/perf.js.map +1 -0
  101. package/dist/harness/sensors.d.ts.map +1 -1
  102. package/dist/harness/sensors.js +13 -1
  103. package/dist/harness/sensors.js.map +1 -1
  104. package/dist/harness/spec-coverage.d.ts +8 -0
  105. package/dist/harness/spec-coverage.d.ts.map +1 -1
  106. package/dist/harness/spec-coverage.js +60 -6
  107. package/dist/harness/spec-coverage.js.map +1 -1
  108. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +51 -3
  109. package/dist/orchestrator/templates/ai-src/commands/create-test.md +10 -0
  110. package/dist/orchestrator/templates/ai-src/commands/run-test.md +23 -0
  111. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
  112. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  113. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +61 -12
  114. package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
  115. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +65 -17
  116. package/dist/orchestrator/templates/qa-context.md +14 -1
  117. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  118. package/dist/orchestrator/templates/specs-api.js +104 -29
  119. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  120. package/dist/orchestrator/templates/specs-api.ts +104 -26
  121. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  122. package/dist/orchestrator/templates/specs-db.js +18 -5
  123. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  124. package/dist/orchestrator/templates/specs-db.ts +19 -5
  125. package/package.json +3 -3
  126. package/src/cli/commands/audit.ts +8 -0
  127. package/src/cli/commands/delivery.ts +14 -2
  128. package/src/cli/commands/inspect.ts +128 -0
  129. package/src/cli/commands/trace.ts +9 -0
  130. package/src/cli/index.ts +2 -0
  131. package/src/exporters/matrix/build.ts +40 -0
  132. package/src/exporters/matrix/export.ts +11 -0
  133. package/src/exporters/matrix/render-xlsx.ts +45 -1
  134. package/src/exporters/matrix/types.ts +10 -0
  135. package/src/exporters/playwright-report-parser.ts +2 -0
  136. package/src/exporters/types.ts +2 -0
  137. package/src/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
  138. package/src/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
  139. package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
  140. package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
  141. package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
  142. package/src/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
  143. package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
  144. package/src/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  145. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
  146. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
  147. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
  148. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
  149. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
  150. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
  151. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
  152. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
  153. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
  154. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
  155. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
  156. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
  157. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
  158. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
  159. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
  160. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
  161. package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
  162. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
  163. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
  164. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
  165. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
  166. package/src/generators/test-generator/code-generator.ts +34 -9
  167. package/src/generators/test-generator/diagnostics.ts +25 -1
  168. package/src/generators/test-generator/patterns/index.ts +172 -25
  169. package/src/generators/test-generator/patterns/types.ts +35 -0
  170. package/src/generators/test-generator/step-mapper.ts +106 -23
  171. package/src/harness/audit.ts +104 -11
  172. package/src/harness/flow-contract.ts +261 -0
  173. package/src/harness/flow-plan.ts +10 -3
  174. package/src/harness/parse.ts +31 -1
  175. package/src/harness/perf.ts +112 -0
  176. package/src/harness/sensors.ts +13 -1
  177. package/src/harness/spec-coverage.ts +55 -5
  178. package/src/orchestrator/templates/ai-src/commands/add-flow.md +51 -3
  179. package/src/orchestrator/templates/ai-src/commands/create-test.md +10 -0
  180. package/src/orchestrator/templates/ai-src/commands/run-test.md +23 -0
  181. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
  182. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  183. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +61 -12
  184. package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
  185. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +65 -17
  186. package/src/orchestrator/templates/qa-context.md +14 -1
  187. package/src/orchestrator/templates/specs-api.ts +104 -26
  188. package/src/orchestrator/templates/specs-db.ts +19 -5
@@ -1,6 +1,6 @@
1
1
  import { ParsedStep } from '../../gherkin-parser';
2
2
  import { MappedStep } from '../step-mapper';
3
- import { StepPattern, PatternContext } from './types';
3
+ import { StepPattern, PatternContext, Platform } from './types';
4
4
  import { capabilityRegistry } from '../../../capabilities/registry';
5
5
  import { discoverAndRegisterCapabilities } from '../../../capabilities/discover';
6
6
 
@@ -21,6 +21,78 @@ export function maskRefsForMatching(text: string): string {
21
21
  .replace(/\[[^\]]*\]/g, '[~]');
22
22
  }
23
23
 
24
+ /**
25
+ * Gesture forms that OWN the word "tap" — each has its own pattern keyed on it (`mobile-double-tap`
26
+ * at priority 17, `tap-top-of-element` at 18) and means MORE than a click. They are the one
27
+ * carve-out from the synonym rule below: leave their text exactly as written.
28
+ */
29
+ const TAP_GESTURE_FORMS = /\bdouble[-\s]?tap|\btap(?:s)?\b[^.]*\b(?:top of|at the top|at top)\b/i;
30
+
31
+ /**
32
+ * `tap` is a synonym of `click` — for MATCHING only.
33
+ *
34
+ * The vocabulary was split: exactly three patterns in `@sungen/driver-mobile` key on "tap", while
35
+ * every other pattern in the shared vocabulary keys on "click". So a mobile-phrased step SILENTLY
36
+ * LOST meaning instead of failing — `User tap [Sign In] button and accept [OK] alert` matched the
37
+ * plain-tap pattern and the alert clause simply vanished, leaving a test that passed having checked
38
+ * less than it claimed. Green tests that assert nothing are the worst outcome in this whole class,
39
+ * which is why this is a synonym rather than 40 duplicated matchers.
40
+ *
41
+ * The rewrite is ADDITIVE — `tap` becomes `tap click`, it does not become `click`. Two reasons, and
42
+ * the first one cost a full golden run to learn:
43
+ *
44
+ * 1. `@sungen/driver-mobile` is registered on WEB projects too (same non-opt-in bundling as
45
+ * driver-ui). Substituting the verb re-keys `mobile-tap` onto "click", where its priority 15
46
+ * outranks the generic `click-element` (10) — so it captured every plain click on every WEB
47
+ * feature and relabelled it a tap. 8 golden fixtures caught it. Adding a verb instead of
48
+ * swapping one means text with no "tap" in it is returned untouched, so web output cannot
49
+ * change here BY CONSTRUCTION rather than by our vigilance.
50
+ * 2. Both vocabularies then see their own verb, and ordinary PRIORITY decides the winner —
51
+ * no matcher has to learn about the other. `tap … and accept alert` reaches
52
+ * `click-with-alert` (21); `tap … in [T] table row 2` reaches `table-action-in-row-nth` (19);
53
+ * a plain `tap` still lands on `mobile-tap` (15) ahead of `click-element` (10).
54
+ *
55
+ * Safe because every click-keyed matcher in the vocabulary is a word-boundary test (`\bclick(s)?\b`
56
+ * / `includes('click')`) — none depends on the verb's neighbours, and none tests for the ABSENCE of
57
+ * "click". Verified across driver-ui, driver-db, driver-mock and driver-mobile before relying on it.
58
+ *
59
+ * Resolvers and generators always receive the ORIGINAL `step.text`, so comments and value
60
+ * extraction never see this string.
61
+ *
62
+ * ORDER IS LOAD-BEARING: this runs AFTER `maskRefsForMatching`, so a selector or variable NAME
63
+ * containing "tap" (a `[Tap Water]` field) has already been blanked and cannot be rewritten into a
64
+ * verb. Reversing the two would reclassify that step as a click. The unit test asserts the order.
65
+ */
66
+ export function normalizeVerbs(text: string): string {
67
+ if (TAP_GESTURE_FORMS.test(text)) return text;
68
+ return text.replace(/\btap(s|ped)?\b/gi, (m) => `${m} click`);
69
+ }
70
+
71
+ /**
72
+ * `TemplateEngine.loadStepTemplate` signals an absent `.hbs` with this exact message
73
+ * (`template-engine.ts`). Matching on it is what separates "this adapter cannot express the step"
74
+ * — a reviewable compile diagnostic — from a genuine bug inside a template, which must keep
75
+ * propagating untouched.
76
+ */
77
+ const TEMPLATE_NOT_FOUND = /^Template not found: (.+)\.hbs in any subfolder$/;
78
+
79
+ /**
80
+ * The step body emitted in place of code the active adapter cannot produce. A comment alone would
81
+ * let the test go GREEN having asserted nothing — the silent-loss class this SPI exists to close —
82
+ * so the step throws at run time as well as reporting at compile time.
83
+ *
84
+ * `JSON.stringify` does the quoting, NOT a hand-rolled `replace(/'/g, …)`. The reason string embeds
85
+ * arbitrary author text (the step sentence), and hand-escaping only the quote character is wrong for
86
+ * any input containing a backslash or a raw newline: a step ending `…\` would emit `'…\\'`, whose
87
+ * trailing escaped-backslash-then-quote breaks the literal and takes the WHOLE generated spec down
88
+ * with a syntax error — reopening, one level deeper, the exact "one bad step kills the file" failure
89
+ * this function exists to prevent. Step text here is routinely non-ASCII (Vietnamese, Japanese),
90
+ * which JSON.stringify also handles correctly.
91
+ */
92
+ function unsupportedStepCode(reason: string): string {
93
+ return `throw new Error(${JSON.stringify(`[sungen] ${reason}`)});`;
94
+ }
95
+
24
96
  /**
25
97
  * Pattern Registry - manages all step patterns
26
98
  */
@@ -59,7 +131,10 @@ export class PatternRegistry {
59
131
  findPattern(step: ParsedStep): StepPattern | null {
60
132
  // Match against text with ref CONTENTS masked, so a verb substring inside a selector/data name
61
133
  // (e.g. "click" in `{{double_click_guard_account.email}}`) can't hijack the verb classification.
62
- const matchStep: ParsedStep = { ...step, text: maskRefsForMatching(step.text) };
134
+ // THEN normalise `tap` `click`, so the mobile phrasing reaches the whole shared vocabulary
135
+ // instead of only the three patterns that happen to key on "tap". Masking first is what keeps a
136
+ // `[Tap Water]` ref from being read as a verb.
137
+ const matchStep: ParsedStep = { ...step, text: normalizeVerbs(maskRefsForMatching(step.text)) };
63
138
  for (const pattern of this.patterns) {
64
139
  if (this.matchesPattern(matchStep, pattern.matcher)) {
65
140
  return pattern;
@@ -73,37 +148,109 @@ export class PatternRegistry {
73
148
  * Prefers resolver (returns template data) over generator (renders directly).
74
149
  */
75
150
  generateStep(step: ParsedStep, context: PatternContext): MappedStep | null {
76
- const pattern = this.findPattern(step);
77
- if (!pattern) {
78
- return null;
79
- }
151
+ // Matchers see only the STEP; some decisions need context (a resolver reading the
152
+ // test-data value, e.g. row-count's numeric check). A resolver may therefore return
153
+ // null to say "not mine after all" — the search continues with the next matching
154
+ // pattern in priority order instead of crashing or silently dropping the step.
155
+ // Matching mirrors findPattern exactly: refs masked FIRST (so `[Tap Water]` is not a
156
+ // verb), then `tap` → `click` normalisation for the mobile vocabulary.
157
+ const matchStep: ParsedStep = { ...step, text: normalizeVerbs(maskRefsForMatching(step.text)) };
158
+ for (const pattern of this.patterns) {
159
+ if (!this.matchesPattern(matchStep, pattern.matcher)) continue;
80
160
 
81
- // Prefer resolver (framework-agnostic) over generator (legacy)
82
- if (pattern.resolver) {
83
- const resolved = pattern.resolver(step, context);
161
+ // Platform gate BEFORE any template lookup. A pattern that declares its platforms and is
162
+ // matched on another one is a vocabulary mismatch, not a missing file: report it in its own
163
+ // terms (SG-W020) rather than letting it fail later as a confusing absent template.
164
+ const platformMiss = PatternRegistry.platformMiss(pattern, context, step);
165
+ if (platformMiss) return platformMiss;
84
166
 
85
- // Auto-inject parent scoping if step has parentRef
86
- // Skip for table-* patterns they resolve the table name internally from step text
87
- if (step.parentRef && step.parentType && !pattern.name.startsWith('table-')) {
88
- resolved.data.parentLocator = PatternRegistry.resolveParentLocator(
89
- step.parentRef, step.parentType, context
90
- );
91
- }
167
+ try {
168
+ // Prefer resolver (framework-agnostic) over generator (legacy)
169
+ if (pattern.resolver) {
170
+ const resolved = pattern.resolver(step, context);
171
+ if (!resolved) continue; // resolver declined — fall through
92
172
 
93
- const code = context.templateEngine.renderStep(resolved.templateName, resolved.data);
94
- return {
95
- code,
96
- comment: resolved.comment,
97
- };
98
- }
173
+ // Auto-inject parent scoping if step has parentRef
174
+ // Skip for table-* patterns — they resolve the table name internally from step text
175
+ if (step.parentRef && step.parentType && !pattern.name.startsWith('table-')) {
176
+ resolved.data.parentLocator = PatternRegistry.resolveParentLocator(
177
+ step.parentRef, step.parentType, context
178
+ );
179
+ }
99
180
 
100
- if (pattern.generator) {
101
- return pattern.generator(step, context);
102
- }
181
+ const code = context.templateEngine.renderStep(resolved.templateName, resolved.data);
182
+ return {
183
+ code,
184
+ comment: resolved.comment,
185
+ };
186
+ }
187
+
188
+ if (pattern.generator) {
189
+ return pattern.generator(step, context);
190
+ }
103
191
 
192
+ return null;
193
+ } catch (err: any) {
194
+ // The step matched a pattern the ACTIVE adapter has no template for. Before this guard the
195
+ // unhandled throw killed the ENTIRE feature file and named neither the step nor the adapter.
196
+ const missing = TEMPLATE_NOT_FOUND.exec(String(err?.message ?? ''));
197
+ if (!missing) throw err;
198
+ return PatternRegistry.unsupportedTemplate(missing[1], pattern, context, step);
199
+ }
200
+ }
104
201
  return null;
105
202
  }
106
203
 
204
+ /**
205
+ * SG-W020 — the matched pattern declares `platforms` and the active one is not among them.
206
+ */
207
+ private static platformMiss(
208
+ pattern: StepPattern, context: PatternContext, step: ParsedStep
209
+ ): MappedStep | null {
210
+ const platform: Platform = context.platform ?? 'web';
211
+ if (!pattern.platforms || pattern.platforms.includes(platform)) return null;
212
+
213
+ const adapter = context.adapterName ?? platform;
214
+ const supported = pattern.platforms.join('/');
215
+ const alternative = pattern.platformAlternative
216
+ ?? 'use a supported step form, or tag the scenario @manual';
217
+ const reason =
218
+ `step "${step.text}" uses pattern "${pattern.name}", which is ${supported}-only — ` +
219
+ `the ${adapter} adapter cannot express it. Instead: ${alternative}.`;
220
+
221
+ context.diagnostics?.add('SG-W020', reason, {
222
+ feature: context.featureName,
223
+ step: step.text,
224
+ });
225
+ return {
226
+ code: unsupportedStepCode(reason),
227
+ comment: `${supported}-only step on the ${adapter} adapter — see SG-W020`,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * SG-E020 — the pattern is in-platform but the active adapter ships no template for it. A gap in
233
+ * the adapter, not in the authored Gherkin: name every coordinate a reader needs to act on it.
234
+ */
235
+ private static unsupportedTemplate(
236
+ templateName: string, pattern: StepPattern, context: PatternContext, step: ParsedStep
237
+ ): MappedStep {
238
+ const adapter = context.adapterName ?? context.platform ?? 'web';
239
+ const reason =
240
+ `step "${step.text}" needs template "${templateName}.hbs", which the ${adapter} adapter ` +
241
+ `does not ship (pattern "${pattern.name}"). Rephrase with a supported step form, or tag ` +
242
+ `the scenario @manual.`;
243
+
244
+ context.diagnostics?.add('SG-E020', reason, {
245
+ feature: context.featureName,
246
+ step: step.text,
247
+ });
248
+ return {
249
+ code: unsupportedStepCode(reason),
250
+ comment: `no ${adapter} template for "${templateName}" — see SG-E020`,
251
+ };
252
+ }
253
+
107
254
  /**
108
255
  * Resolve parent scoping to a Playwright locator string.
109
256
  * Tries YAML lookup first, falls back to auto-infer from parentType.
@@ -3,6 +3,22 @@ import { MappedStep } from '../step-mapper';
3
3
  import { SelectorResolver } from '../utils/selector-resolver';
4
4
  import { DataResolver } from '../utils/data-resolver';
5
5
 
6
+ /**
7
+ * The subset of DiagnosticsCollector the pattern layer needs. Structural, not a class import,
8
+ * so the framework-agnostic pattern SPI keeps depending on nothing concrete.
9
+ */
10
+ export interface DiagnosticsSink {
11
+ add(code: any, message: string, context?: { feature?: string; step?: string }): void;
12
+ }
13
+
14
+ /**
15
+ * The platform a pattern (and its templates) can compile for. Drawn from the ACTIVE adapter:
16
+ * `appium` → 'mobile', everything else → 'web'. A pattern that omits `platforms` is assumed to
17
+ * work on BOTH — the historical assumption, kept as the default so no existing pattern changes
18
+ * behaviour.
19
+ */
20
+ export type Platform = 'web' | 'mobile';
21
+
6
22
  /**
7
23
  * Pattern matcher function or regex
8
24
  */
@@ -44,6 +60,12 @@ export interface PatternContext {
44
60
  scenarioSteps?: ParsedStep[];
45
61
  /** Effective Gherkin keyword (Given/When/Then), resolved from And/But */
46
62
  effectiveKeyword?: string;
63
+ /** The platform the ACTIVE adapter compiles for. Absent = 'web' (back-compat default). */
64
+ platform?: Platform;
65
+ /** The adapter's own name ('playwright' | 'appium' | …) — quoted verbatim in SG-E020/SG-W020. */
66
+ adapterName?: string;
67
+ /** Compile-diagnostics channel, so the pattern layer can report instead of throwing. */
68
+ diagnostics?: DiagnosticsSink;
47
69
  /** App profile (qa/app.yaml merged over presets/defaults) — widget variants, recipes,
48
70
  * loading indicator, table row conventions. Typed loosely to keep the SPI decoupled;
49
71
  * the concrete shape is ResolvedAppProfile. */
@@ -69,4 +91,17 @@ export interface StepPattern {
69
91
  resolver?: PatternResolver;
70
92
  generator?: PatternGenerator;
71
93
  priority?: number;
94
+ /**
95
+ * Platforms this pattern can compile for. ABSENT = both (the default — every pattern that
96
+ * predates the platform SPI). Declaring it narrows the pattern: matching it on any other
97
+ * platform emits SG-W020 and compiles to a loud runtime `throw` instead of silently
98
+ * producing code that references a driver the platform does not have.
99
+ */
100
+ platforms?: ReadonlyArray<Platform>;
101
+ /**
102
+ * Free-text alternative named in the SG-W020 message when this pattern is used off-platform
103
+ * (e.g. "`@api` / `@query`, or tag the scenario @manual"). Optional — a generic pointer is
104
+ * emitted when absent.
105
+ */
106
+ platformAlternative?: string;
72
107
  }
@@ -1,7 +1,7 @@
1
1
  import { ParsedStep } from '../gherkin-parser';
2
2
  import { TemplateEngine } from './template-engine';
3
3
  import { adapterRegistry } from './adapters';
4
- import { PatternRegistry, PatternContext } from './patterns';
4
+ import { PatternRegistry, PatternContext, Platform } from './patterns';
5
5
  import { SelectorResolver } from './utils/selector-resolver';
6
6
  import { DataResolver } from './utils/data-resolver';
7
7
  import { DiagnosticsCollector } from './diagnostics';
@@ -49,6 +49,10 @@ export class StepMapper {
49
49
  private diagnostics?: DiagnosticsCollector;
50
50
  /** App profile (qa/app.yaml merged over presets/defaults) — the per-project BEHAVIOR layer */
51
51
  private appProfile: ResolvedAppProfile;
52
+ /** The platform the ACTIVE adapter compiles for — drives the pattern-layer platform gate */
53
+ private platform: Platform;
54
+ /** The active adapter's own name ('playwright' | 'appium') — quoted in SG-E020/SG-W020 */
55
+ private adapterName: string;
52
56
 
53
57
  constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string; runtimeData?: boolean; framework?: string } = {}) {
54
58
  this.verbose = options.verbose ?? false;
@@ -60,7 +64,13 @@ export class StepMapper {
60
64
  // mobile→Appium). The adapter owns its templatesDir; resolving it here keeps step
61
65
  // rendering and the file skeleton (code-generator) on the same framework.
62
66
  const framework = options.framework || 'web';
63
- const templatesDir = adapterRegistry.getAdapter(framework).templatesDir;
67
+ const adapter = adapterRegistry.getAdapter(framework);
68
+ const templatesDir = adapter.templatesDir;
69
+ // The platform gate keys on the ADAPTER, not on the requested framework name: 'mobile' and
70
+ // 'appium' are aliases of the same adapter, so asking the adapter itself keeps one source of
71
+ // truth as more aliases appear.
72
+ this.adapterName = adapter.frameworkName;
73
+ this.platform = adapter.frameworkName === 'appium' ? 'mobile' : 'web';
64
74
  this.templateEngine = new TemplateEngine(templatesDir);
65
75
  this.patternRegistry = new PatternRegistry();
66
76
  this.selectorResolver = new SelectorResolver(undefined, options.screenName);
@@ -175,6 +185,28 @@ export class StepMapper {
175
185
  this.lastPrimaryKeyword = keyword;
176
186
  }
177
187
 
188
+ // SG-W015 — `see [X] card with {{n}}` where n is a bare number compiles to a TEXT
189
+ // assertion (toHaveText('3')), not a count. An author reaching for "3 cards are shown"
190
+ // gets an assertion that can only pass if the card's TEXT is the digit — a wrong test
191
+ // with no error. Count semantics live on row/table ("see [T] table with {{n}}",
192
+ // "see [Ref] row with {{n}}") and lists ("should have count"); on the container types
193
+ // below, a bare-number text expectation is almost never what the author meant. A text
194
+ // element asserting a numeric readout ("[Row Quantity] text with 4") stays silent.
195
+ const COUNT_MISREAD_TYPES = new Set(['card', 'item', 'items', 'cards', 'option', 'options']);
196
+ if (this.diagnostics && step.dataRef && step.selectorRef &&
197
+ COUNT_MISREAD_TYPES.has((step.elementType || '').toLowerCase()) &&
198
+ /\bsees?\s+\[/.test(step.text) && /\bwith\b/.test(step.text)) {
199
+ // peekBaseValue reads the literal from test-data even in runtime-data mode,
200
+ // where resolveData returns an opaque marker (never numeric-looking).
201
+ let resolvedValue: string | undefined;
202
+ try { resolvedValue = this.dataResolver.peekBaseValue(step.dataRef, this.featureName); } catch { /* unresolved at compile time → cannot judge */ }
203
+ if (resolvedValue !== undefined && /^\d+$/.test(resolvedValue.trim())) {
204
+ this.diagnostics.add('SG-W015',
205
+ `\`see [${step.selectorRef}] ${step.elementType} with {{${step.dataRef}}}\` compiles to a TEXT assertion toHaveText('${resolvedValue.trim()}'), NOT a count of ${step.elementType}s. For a count use \`see [Ref] row with {{n}}\` / \`see [T] table with {{n}}\` (or a list "should have count"); if '${resolvedValue.trim()}' really is the ${step.elementType}'s text, assert it on a text/label element instead`,
206
+ { feature: this.featureName, step: step.text });
207
+ }
208
+ }
209
+
178
210
  // SG-W007 — a two-step-form browser-alert handler (`click [OK] alert`) written AFTER
179
211
  // an action step registers its page.once('dialog') listener too late: a dialog
180
212
  // triggered by the earlier action auto-dismisses with no error (issue #472).
@@ -212,10 +244,12 @@ export class StepMapper {
212
244
  const selectorRef = step.selectorRef || '';
213
245
 
214
246
  if (selectorRef.toLowerCase() === 'main') {
215
- // Exit frame scope
247
+ // Exit frame scope — routed through the adapter (C1): the Appium twin performs the
248
+ // real webview→native context switch; a bare comment here would silently no-op on mobile.
216
249
  this.inFrameScope = false;
217
250
  this.templateEngine.setBaseContext({ inFrame: false });
218
- return { code: '// Back to main page context', comment: 'Exit frame scope' };
251
+ const code = this.templateEngine.renderStep('frame-exit-action', {});
252
+ return { code, comment: 'Exit frame scope' };
219
253
  }
220
254
 
221
255
  // Enter frame scope
@@ -228,7 +262,9 @@ export class StepMapper {
228
262
  // Use selectorRef as-is
229
263
  }
230
264
  this.templateEngine.setBaseContext({ inFrame: true, frameSelector });
231
- const code = `const frame = page.frameLocator('${frameSelector}');`;
265
+ // C1 routed through the adapter instead of building `page.frameLocator(...)` inline:
266
+ // the Appium adapter's own template performs its webview-context switch instead.
267
+ const code = this.templateEngine.renderStep('frame-enter-action', { frameSelector });
232
268
  return { code, comment: `Switch to ${selectorRef} frame` };
233
269
  }
234
270
 
@@ -236,6 +272,7 @@ export class StepMapper {
236
272
  if (/\buse dialog\b/i.test(step.text)) {
237
273
  this.inDialogScope = true;
238
274
  this.templateEngine.setBaseContext({ inDialog: true });
275
+ this.warnDialogScopeIgnoredOnMobile(step);
239
276
  return { code: '', comment: 'Enter dialog scope' };
240
277
  }
241
278
  if (/\b(?:close|dismiss|exit)\s+dialog\b/i.test(step.text)) {
@@ -246,6 +283,7 @@ export class StepMapper {
246
283
  // Given User is on [...] dialog — set dialog scope (3 cases)
247
284
  if (step.text.includes('is on') && step.elementType === 'dialog') {
248
285
  this.inDialogScope = true;
286
+ this.warnDialogScopeIgnoredOnMobile(step);
249
287
  // setBaseContext MERGES — clear every root key so a previous dialog scope in the
250
288
  // same scenario cannot leak its root into this one.
251
289
  const contextVars: Record<string, any> = {
@@ -333,6 +371,7 @@ export class StepMapper {
333
371
  console.log(` ✓ Pattern matched: ${step.text.substring(0, 50)}...`);
334
372
  }
335
373
  this.checkDataRefConsumed(step, mappedStep);
374
+ this.checkPageAssertionNoOpOnMobile(step, mappedStep);
336
375
  this.checkPageAssertionFallback(step, mappedStep);
337
376
  this.checkDeclaredUrlHasNoQuery(step, mappedStep);
338
377
  return mappedStep;
@@ -493,6 +532,11 @@ export class StepMapper {
493
532
  * includeHidden + visible filter: when a modal (e.g. Radix Dialog) is open the
494
533
  * background gets aria-hidden="true", which empties plain getByRole() even though
495
534
  * the cells are still rendered — measure what the user SEES, not the a11y tree.
535
+ *
536
+ * C2 — this method used to build the Playwright code string itself (referencing `tableRow`,
537
+ * a binding that only exists in the Playwright output). It now only gathers data and routes
538
+ * through the adapter template; the Appium twin throws (HTML table roles), matching the
539
+ * `table-*` house style.
496
540
  */
497
541
  private generateRowScopedColumnAssertion(step: ParsedStep): MappedStep {
498
542
  const columnRef = step.selectorRef || '';
@@ -515,24 +559,9 @@ export class StepMapper {
515
559
  }
516
560
 
517
561
  const isGiven = this.lastPrimaryKeyword === 'Given';
518
- const escapedValue = cellValue.replace(/'/g, "\\'");
519
-
520
- let code: string;
521
- if (columnIndex !== undefined) {
522
- // Exact cell: nth(index) + toHaveText
523
- if (isGiven) {
524
- code = `await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ visible: true }).nth(${columnIndex})).toHaveText('${escapedValue}');`;
525
- } else {
526
- code = `await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ visible: true }).nth(${columnIndex})).toHaveText('${escapedValue}');`;
527
- }
528
- } else {
529
- // Fallback: filter by text
530
- if (isGiven) {
531
- code = `await tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '${escapedValue}' }).filter({ visible: true }).waitFor();`;
532
- } else {
533
- code = `await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '${escapedValue}' }).filter({ visible: true })).toBeVisible();`;
534
- }
535
- }
562
+ const code = this.templateEngine.renderStep('row-scoped-column-assertion', {
563
+ columnIndex, cellValue, isGiven,
564
+ });
536
565
 
537
566
  return {
538
567
  code,
@@ -543,6 +572,57 @@ export class StepMapper {
543
572
  /**
544
573
  * Create pattern context for generators
545
574
  */
575
+ /**
576
+ * SG-W022 — a `page` assertion that emitted no executable code on mobile.
577
+ *
578
+ * A native app has no URL, so the Appium `page-assertion` template is a bare comment. The step
579
+ * therefore READS as an assertion and checks nothing: the scenario passes whatever the app is
580
+ * showing. That is strictly worse than the missing-template crashes this pass closed — a crash is
581
+ * visible on the first run, a false pass hides until someone trusts it. (Its `is on [X] page` twin
582
+ * routes to `route-assertion`, which throws loudly; only this half was silent.)
583
+ *
584
+ * Keyed on the EMITTED CODE rather than on a template name, so any future page-assertion path
585
+ * that degrades to a comment is caught by the same check without being enumerated here. Scoped to
586
+ * the `see` form: the `is on` twin is mobile's app-launch directive, whose empty output is right.
587
+ */
588
+ private checkPageAssertionNoOpOnMobile(step: ParsedStep, mapped: MappedStep): void {
589
+ if (this.platform !== 'mobile' || !this.diagnostics) return;
590
+ if (step.elementType !== 'page') return;
591
+ // Only the ASSERTION form. `Given User is on [X] page` is the app-LAUNCH directive on mobile —
592
+ // it emits a comment because wdio capabilities already launched the app, which is correct and
593
+ // must not be warned about. `Then User see [X] page` is the one that claims to check something.
594
+ if (!/\bsees?\b/i.test(step.text)) return;
595
+ // Strip comments; whatever remains is what actually runs. Block comments are handled as well as
596
+ // line ones: a template written with `/* … */` would otherwise read as executable code and
597
+ // silently defeat this check — the diagnostic would go quiet exactly when it was needed.
598
+ const executable = (mapped.code || '')
599
+ .replace(/\/\*[\s\S]*?\*\//g, '')
600
+ .split('\n')
601
+ .map((l) => l.replace(/\/\/.*$/, '').trim())
602
+ .filter(Boolean)
603
+ .join('');
604
+ if (executable) return;
605
+ this.diagnostics.add('SG-W022',
606
+ 'a native app has no URL, so this page assertion compiled to a COMMENT — it asserts nothing and the scenario passes regardless. Assert a marker that is actually on the screen instead (`Then User see [Some Element] ...`), or tag the scenario @manual',
607
+ { feature: this.featureName, step: step.text });
608
+ }
609
+
610
+ /**
611
+ * SG-W021 — dialog scope is a no-op on the Appium adapter.
612
+ *
613
+ * `setBaseContext({ inDialog: true })` records the scope faithfully, and every Playwright locator
614
+ * partial consults it. No Appium template does. So on mobile the step reads as "narrow everything
615
+ * that follows to this dialog" and in fact narrows nothing — the following steps search the whole
616
+ * screen. Scope that is silently discarded is what produced this entire defect class, so it is
617
+ * reported rather than left to be discovered from a passing test.
618
+ */
619
+ private warnDialogScopeIgnoredOnMobile(step: ParsedStep): void {
620
+ if (this.platform !== 'mobile') return;
621
+ this.diagnostics?.add('SG-W021',
622
+ 'dialog scope has no effect on the mobile adapter — no Appium template reads `inDialog`, so the following steps resolve against the whole screen, not the dialog. Scope the elements themselves instead (a `scope: dialog` entry, or accessibility-ids unique to the dialog)',
623
+ { feature: this.featureName, step: step.text });
624
+ }
625
+
546
626
  private createPatternContext(): PatternContext {
547
627
  return {
548
628
  stepCounter: this.stepCounter,
@@ -555,6 +635,9 @@ export class StepMapper {
555
635
  dataResolver: this.dataResolver,
556
636
  renderLocator: (resolved) => this.templateEngine.renderStep('locator', resolved),
557
637
  appProfile: this.appProfile,
638
+ platform: this.platform,
639
+ adapterName: this.adapterName,
640
+ diagnostics: this.diagnostics,
558
641
  baseURL: this.baseURL,
559
642
  featureName: this.featureName,
560
643
  screenName: this.screenName,