@sun-asterisk/sungen 3.2.22-beta.1 → 3.2.22

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 (101) hide show
  1. package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
  2. package/dist/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
  3. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
  4. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
  5. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
  6. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
  7. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
  8. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  9. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
  10. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
  11. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
  12. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
  13. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
  14. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
  15. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
  16. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
  17. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
  18. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
  19. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
  20. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
  21. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
  22. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
  23. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
  24. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
  25. package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
  26. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
  27. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
  28. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
  29. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
  30. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  31. package/dist/generators/test-generator/code-generator.js +29 -8
  32. package/dist/generators/test-generator/code-generator.js.map +1 -1
  33. package/dist/generators/test-generator/diagnostics.d.ts +20 -1
  34. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  35. package/dist/generators/test-generator/diagnostics.js +19 -0
  36. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  37. package/dist/generators/test-generator/patterns/index.d.ts +45 -0
  38. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  39. package/dist/generators/test-generator/patterns/index.js +150 -19
  40. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  41. package/dist/generators/test-generator/patterns/types.d.ts +36 -0
  42. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  43. package/dist/generators/test-generator/step-mapper.d.ts +33 -0
  44. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  45. package/dist/generators/test-generator/step-mapper.js +78 -24
  46. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  47. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
  48. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  49. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +61 -12
  50. package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
  51. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -1
  52. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  53. package/dist/orchestrator/templates/specs-api.js +104 -29
  54. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  55. package/dist/orchestrator/templates/specs-api.ts +104 -26
  56. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  57. package/dist/orchestrator/templates/specs-db.js +18 -5
  58. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  59. package/dist/orchestrator/templates/specs-db.ts +19 -5
  60. package/package.json +3 -3
  61. package/src/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
  62. package/src/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
  63. package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
  64. package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
  65. package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
  66. package/src/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
  67. package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
  68. package/src/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  69. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
  70. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
  71. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
  72. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
  73. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
  74. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
  75. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
  76. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
  77. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
  78. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
  79. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
  80. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
  81. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
  82. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
  83. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
  84. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
  85. package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
  86. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
  87. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
  88. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
  89. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
  90. package/src/generators/test-generator/code-generator.ts +34 -9
  91. package/src/generators/test-generator/diagnostics.ts +20 -1
  92. package/src/generators/test-generator/patterns/index.ts +165 -24
  93. package/src/generators/test-generator/patterns/types.ts +35 -0
  94. package/src/generators/test-generator/step-mapper.ts +84 -23
  95. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
  96. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  97. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +61 -12
  98. package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
  99. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -1
  100. package/src/orchestrator/templates/specs-api.ts +104 -26
  101. package/src/orchestrator/templates/specs-db.ts +19 -5
@@ -0,0 +1 @@
1
+ {{!-- Column assertion scoped to the current row (`tableRow`, bound by the row-in-table entry step that ran before this one). columnIndex uses `isDefined` rather than a bare truthy check: column 0 is a valid index. --}}{{#if (isDefined columnIndex)}}await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ visible: true }).nth({{columnIndex}})).toHaveText('{{escapeQuotes cellValue}}');{{else if isGiven}}await tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '{{escapeQuotes cellValue}}' }).filter({ visible: true }).waitFor();{{else}}await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '{{escapeQuotes cellValue}}' }).filter({ visible: true })).toBeVisible();{{/if}}
@@ -72,23 +72,34 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
72
72
  }
73
73
 
74
74
  /**
75
- * Extract pass-through tags (non-functional) for Playwright { tag: [...] }.
76
- * Any tag not recognized by sungen as functional → pass through.
75
+ * Extract pass-through tags (non-functional) the unique, unformatted tag tokens (e.g. `@smoke`).
76
+ * Any tag not recognized by sungen as functional → pass through. Formatting is adapter-specific
77
+ * (Playwright wants a quoted `{ tag: [...] }` array; Appium/mocha has no such option and instead
78
+ * appends the tags to the `it()` title — see `formatPassThroughTags`).
77
79
  */
78
80
  const FUNCTIONAL_TAG_PREFIXES = [
79
81
  '@parallel', '@serial', '@cleanup:', '@auth:', '@manual', '@no-auth',
80
82
  '@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
81
- '@flow', '@cases:', '@deferred:', '@owned-by:',
83
+ '@flow', '@cases:', '@deferred:', '@owned-by:', '@platform:',
82
84
  ];
83
85
 
84
- function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string | undefined {
86
+ function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string[] {
85
87
  const allTags = [...featureTags, ...scenarioTags];
86
88
  const passThrough = allTags.filter(tag =>
87
89
  !FUNCTIONAL_TAG_PREFIXES.some(prefix => tag.startsWith(prefix))
88
90
  );
89
- const unique = [...new Set(passThrough)];
90
- if (unique.length === 0) return undefined;
91
- return unique.map(t => `'${t}'`).join(', ');
91
+ return [...new Set(passThrough)];
92
+ }
93
+
94
+ /**
95
+ * Format the unique pass-through tags for the active adapter's `ScenarioData.tags` field:
96
+ * - Playwright: `'@smoke', '@high'` — spliced verbatim into `{ tag: [...] }` (see scenario.hbs).
97
+ * - Appium: `@smoke @high` — appended to the mocha `it()` title (D5) so `--grep` still filters,
98
+ * since mocha has no per-test tag/annotation option like Playwright's.
99
+ */
100
+ function formatPassThroughTags(tags: string[], frameworkName: string): string | undefined {
101
+ if (tags.length === 0) return undefined;
102
+ return frameworkName === 'appium' ? tags.join(' ') : tags.map(t => `'${t}'`).join(', ');
92
103
  }
93
104
 
94
105
  /**
@@ -712,6 +723,19 @@ export class CodeGenerator {
712
723
  // - Multiple groups: nested describes per auth role
713
724
  const needsGrouping = authGroups.length > 1;
714
725
 
726
+ // D6 — the Appium test-file template has no per-group before-hook (only `singleAuthRole` runs
727
+ // `__ensureAuth`); >1 auth group would render every group's scenarios with NO authentication at
728
+ // all, silently. Mirrors the web `@parallel` guard below, but unconditional (isParallel has no
729
+ // per-group meaning on mobile — the template never branches on it).
730
+ if (needsGrouping && this.adapter.frameworkName === 'appium') {
731
+ const roles = groupOrder.map(k => k || '(no @auth)').join(', ');
732
+ throw new Error(
733
+ `Feature "${feature.name}" has multiple auth groups (${roles}) but the Appium adapter only ` +
734
+ `supports a single shared auth role per feature — the other groups would run unauthenticated.\n` +
735
+ `Fix: split "${feature.name}" into one feature file per @auth role.`
736
+ );
737
+ }
738
+
715
739
  if (needsGrouping && !isParallel) {
716
740
  throw new Error(
717
741
  `Feature "${feature.name}" has multiple auth groups but no @parallel tag.\n` +
@@ -968,8 +992,9 @@ export class CodeGenerator {
968
992
  }
969
993
  }
970
994
 
971
- // Extract pass-through tags (feature + scenario, excluding functional tags)
972
- const tags = extractPassThroughTags(scenario.tags, featureTags);
995
+ // Extract pass-through tags (feature + scenario, excluding functional tags), formatted for
996
+ // the active adapter (Playwright `{ tag: [...] }` vs. Appium's mocha-title suffix — D5).
997
+ const tags = formatPassThroughTags(extractPassThroughTags(scenario.tags, featureTags), this.adapter.frameworkName);
973
998
 
974
999
  // Use adapter to render scenario
975
1000
  const rendered = this.adapter.renderScenario({
@@ -40,12 +40,31 @@
40
40
  * "3 cards are shown" gets an assertion that can only pass if the card's text
41
41
  * IS the digit. Count semantics exist on row/table ("see [T] table with {{n}}",
42
42
  * "see [Ref] row with {{n}}") and lists ("should have count") only
43
+ * SG-E020 — the step matched a pattern whose template the ACTIVE adapter does not ship. Before
44
+ * the platform SPI this was an unhandled `Template not found` that killed the WHOLE
45
+ * feature file and named neither the step nor the adapter. The step now compiles to a
46
+ * loud `throw`, the rest of the file still generates, and the message names the step,
47
+ * the feature, the pattern, the template and the adapter
48
+ * SG-W020 — the step matched a pattern that DECLARES its platforms (`platforms: ['web']`) and the
49
+ * active platform is not among them — e.g. `@mock` route interception on mobile. Caught
50
+ * before any template lookup, so it reports as a vocabulary mismatch rather than as a
51
+ * confusing missing file, and names the native alternative
52
+ * SG-W021 — `use dialog` (or `User is on [X] dialog`) under the mobile adapter: the scope is
53
+ * recorded in the template base context, but NO Appium template reads `inDialog`, so
54
+ * every following step resolves page-wide as if the scope had never been written.
55
+ * Silently-ignored scope is the exact defect class this channel exists for — the step
56
+ * LOOKS like it narrows the search and does not
57
+ * SG-W022 — a `page` assertion on the mobile adapter that emitted NO executable code. A native app
58
+ * has no URL, so `Then User see [X] page` compiles to a bare comment: the step reads as
59
+ * an assertion, checks nothing, and the scenario PASSES regardless. Worse than the
60
+ * crash class this plan closed — a crash is visible, a false pass is not. Its `is on`
61
+ * twin routes to `route-assertion`, which throws; this half was silent
43
62
  *
44
63
  * Diagnostics never block generation by themselves; `sungen generate --strict`
45
64
  * turns any collected diagnostic into a non-zero exit.
46
65
  */
47
66
 
48
- export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014' | 'SG-W015';
67
+ export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014' | 'SG-W015' | 'SG-E020' | 'SG-W020' | 'SG-W021' | 'SG-W022';
49
68
 
50
69
  export interface Diagnostic {
51
70
  code: DiagnosticCode;
@@ -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;
@@ -77,39 +152,105 @@ export class PatternRegistry {
77
152
  // test-data value, e.g. row-count's numeric check). A resolver may therefore return
78
153
  // null to say "not mine after all" — the search continues with the next matching
79
154
  // pattern in priority order instead of crashing or silently dropping the step.
80
- const matchStep: ParsedStep = { ...step, text: maskRefsForMatching(step.text) };
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)) };
81
158
  for (const pattern of this.patterns) {
82
159
  if (!this.matchesPattern(matchStep, pattern.matcher)) continue;
83
160
 
84
- // Prefer resolver (framework-agnostic) over generator (legacy)
85
- if (pattern.resolver) {
86
- const resolved = pattern.resolver(step, context);
87
- if (!resolved) continue; // resolver declined — fall through
88
-
89
- // Auto-inject parent scoping if step has parentRef
90
- // Skip for table-* patterns — they resolve the table name internally from step text
91
- if (step.parentRef && step.parentType && !pattern.name.startsWith('table-')) {
92
- resolved.data.parentLocator = PatternRegistry.resolveParentLocator(
93
- step.parentRef, step.parentType, context
94
- );
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;
166
+
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
172
+
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
+ }
180
+
181
+ const code = context.templateEngine.renderStep(resolved.templateName, resolved.data);
182
+ return {
183
+ code,
184
+ comment: resolved.comment,
185
+ };
95
186
  }
96
187
 
97
- const code = context.templateEngine.renderStep(resolved.templateName, resolved.data);
98
- return {
99
- code,
100
- comment: resolved.comment,
101
- };
102
- }
188
+ if (pattern.generator) {
189
+ return pattern.generator(step, context);
190
+ }
103
191
 
104
- if (pattern.generator) {
105
- return pattern.generator(step, context);
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);
106
199
  }
107
-
108
- return null;
109
200
  }
110
201
  return null;
111
202
  }
112
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
+
113
254
  /**
114
255
  * Resolve parent scoping to a Playwright locator string.
115
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);
@@ -234,10 +244,12 @@ export class StepMapper {
234
244
  const selectorRef = step.selectorRef || '';
235
245
 
236
246
  if (selectorRef.toLowerCase() === 'main') {
237
- // 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.
238
249
  this.inFrameScope = false;
239
250
  this.templateEngine.setBaseContext({ inFrame: false });
240
- 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' };
241
253
  }
242
254
 
243
255
  // Enter frame scope
@@ -250,7 +262,9 @@ export class StepMapper {
250
262
  // Use selectorRef as-is
251
263
  }
252
264
  this.templateEngine.setBaseContext({ inFrame: true, frameSelector });
253
- 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 });
254
268
  return { code, comment: `Switch to ${selectorRef} frame` };
255
269
  }
256
270
 
@@ -258,6 +272,7 @@ export class StepMapper {
258
272
  if (/\buse dialog\b/i.test(step.text)) {
259
273
  this.inDialogScope = true;
260
274
  this.templateEngine.setBaseContext({ inDialog: true });
275
+ this.warnDialogScopeIgnoredOnMobile(step);
261
276
  return { code: '', comment: 'Enter dialog scope' };
262
277
  }
263
278
  if (/\b(?:close|dismiss|exit)\s+dialog\b/i.test(step.text)) {
@@ -268,6 +283,7 @@ export class StepMapper {
268
283
  // Given User is on [...] dialog — set dialog scope (3 cases)
269
284
  if (step.text.includes('is on') && step.elementType === 'dialog') {
270
285
  this.inDialogScope = true;
286
+ this.warnDialogScopeIgnoredOnMobile(step);
271
287
  // setBaseContext MERGES — clear every root key so a previous dialog scope in the
272
288
  // same scenario cannot leak its root into this one.
273
289
  const contextVars: Record<string, any> = {
@@ -355,6 +371,7 @@ export class StepMapper {
355
371
  console.log(` ✓ Pattern matched: ${step.text.substring(0, 50)}...`);
356
372
  }
357
373
  this.checkDataRefConsumed(step, mappedStep);
374
+ this.checkPageAssertionNoOpOnMobile(step, mappedStep);
358
375
  this.checkPageAssertionFallback(step, mappedStep);
359
376
  this.checkDeclaredUrlHasNoQuery(step, mappedStep);
360
377
  return mappedStep;
@@ -515,6 +532,11 @@ export class StepMapper {
515
532
  * includeHidden + visible filter: when a modal (e.g. Radix Dialog) is open the
516
533
  * background gets aria-hidden="true", which empties plain getByRole() even though
517
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.
518
540
  */
519
541
  private generateRowScopedColumnAssertion(step: ParsedStep): MappedStep {
520
542
  const columnRef = step.selectorRef || '';
@@ -537,24 +559,9 @@ export class StepMapper {
537
559
  }
538
560
 
539
561
  const isGiven = this.lastPrimaryKeyword === 'Given';
540
- const escapedValue = cellValue.replace(/'/g, "\\'");
541
-
542
- let code: string;
543
- if (columnIndex !== undefined) {
544
- // Exact cell: nth(index) + toHaveText
545
- if (isGiven) {
546
- code = `await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ visible: true }).nth(${columnIndex})).toHaveText('${escapedValue}');`;
547
- } else {
548
- code = `await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ visible: true }).nth(${columnIndex})).toHaveText('${escapedValue}');`;
549
- }
550
- } else {
551
- // Fallback: filter by text
552
- if (isGiven) {
553
- code = `await tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '${escapedValue}' }).filter({ visible: true }).waitFor();`;
554
- } else {
555
- code = `await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '${escapedValue}' }).filter({ visible: true })).toBeVisible();`;
556
- }
557
- }
562
+ const code = this.templateEngine.renderStep('row-scoped-column-assertion', {
563
+ columnIndex, cellValue, isGiven,
564
+ });
558
565
 
559
566
  return {
560
567
  code,
@@ -565,6 +572,57 @@ export class StepMapper {
565
572
  /**
566
573
  * Create pattern context for generators
567
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
+
568
626
  private createPatternContext(): PatternContext {
569
627
  return {
570
628
  stepCounter: this.stepCounter,
@@ -577,6 +635,9 @@ export class StepMapper {
577
635
  dataResolver: this.dataResolver,
578
636
  renderLocator: (resolved) => this.templateEngine.renderStep('locator', resolved),
579
637
  appProfile: this.appProfile,
638
+ platform: this.platform,
639
+ adapterName: this.adapterName,
640
+ diagnostics: this.diagnostics,
580
641
  baseURL: this.baseURL,
581
642
  featureName: this.featureName,
582
643
  screenName: this.screenName,
@@ -79,10 +79,10 @@ A flow (`create → login → delete`) is a **Functional integration** test, **n
79
79
  - ":gift_image_1" # test-data: gift_image_1 → fixtures/a.png
80
80
  - ":gift_image_2" # test-data: gift_image_2 → fixtures/b.png
81
81
  ```
82
- Each element binds its own `:param` from test-data. Use this for endpoints that accept a list of files under the same field — a single object value can only hold one file. **Automate multi-file uploads with `@api`; don't defer to `@manual`.** (File uploads use Playwright's `FormData` multipart, which requires **`@playwright/test` 1.44** the version sungen installs by default; only projects pinned to an older Playwright need to upgrade.)
82
+ Each element binds its own `:param` from test-data. Use this for endpoints that accept a list of files under the same field — a single object value can only hold one file. **Automate multi-file uploads with `@api`; don't defer to `@manual`.** (File uploads are sent as `multipart/form-data` built on Node's global `FormData`/`Blob` **Node 18+, no Playwright dependency**, so the same upload runs in mobile specs too.)
83
83
  - **`bodyFile:`** → raw binary body (the whole body IS the file's bytes, e.g. `application/octet-stream`): `bodyFile: { path: ":image", mimeType: application/octet-stream }`.
84
84
 
85
- Fixture path resolves cwd-relative/absolute first, else `qa/fixtures/<path>` (drop sample files there, reference by name from `test-data`). An empty resolved file param omits the part → use for missing-file `@cases` error rows. `files:`/`bodyFile:` are mutually exclusive. **Automate the upload success case with `@api`** — don't defer it to `@manual`.
85
+ Fixture path resolves cwd-relative/absolute first, else `qa/fixtures/<path>` (drop sample files there, reference by name from `test-data`). An empty resolved file param omits the part → use for missing-file `@cases` error rows. `files:`/`bodyFile:` are mutually exclusive. A **`GET`/`HEAD` entry may carry no body at all** (no `body:`/`files:`/`bodyFile:`) — HTTP forbids it and the catalog lint refuses it; put the inputs in the path or `query`, or use `POST`. **Automate the upload success case with `@api`** — don't defer it to `@manual`.
86
86
 
87
87
  ## Per-endpoint knobs & auth patterns
88
88
  - **Timeout** — a slow endpoint can override the datasource default (15s) with `timeout_ms: 30000` on its catalog entry (else the datasource `timeout_ms` applies).
@@ -99,6 +99,10 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
99
99
  | `SG-W012` | A mock-install step written AFTER a navigation step in the same block — `page.route()` registered after `goto()` misses every request fired during page load | Move the mock-install step before the navigation, or into `Background` |
100
100
  | `SG-W013` | A page assertion (`see [X] page` / `is on [X] page`) whose `[Ref]` has no `type: page` selector entry (or collides with a non-page entry) — the step falls back to the feature's own path (or `/<ref>/`) instead of `X`'s real URL, so the anchored assertion can never pass | Declare a `type: page` entry for `[Ref]` with its real URL; if the key collides with another type, disambiguate with a `--type` suffix (`sungen-selector-keys` § Collision rule) |
101
101
  | `SG-W014` | `[X] page with {{v}}` where `{{v}}`'s base test-data value carries no query and no fragment — the step checks the PATH only, asserting less than it reads as | Informational — pass a value like `?q=…` if you meant to assert a query, or drop `with {{v}}` for a bare page |
102
+ | `SG-E020` | A step matched a pattern, but the **active adapter ships no template** for it (e.g. a web-only step compiled under `platform: mobile`). The feature file still generates — that one step compiles to `throw new Error("[sungen] …")` naming the step, feature, pattern, template and adapter, so the failure is loud and traceable rather than a crashed build | Rephrase to a step the target adapter actually ships (see `sungen-gherkin-syntax` Platform Support section / `sungen-mobile-gestures`), or tag the scenario `@manual` with the platform reason |
103
+ | `SG-W020` | The matched pattern **declares `platforms`** (today: `@mock`) and the active platform isn't among them. Caught before template lookup, so the diagnostic can name the native alternative directly | Drop `@mock` from a mobile unit (there is no Mock Driver on Appium) — use `@api`/`@query` or a `@manual` note instead |
104
+ | `SG-W021` | `use dialog` / `User is on [X] dialog` scope under the **mobile** adapter — the scope is recorded but no Appium template reads `inDialog`, so every following step resolves against the whole screen, not just the dialog | Don't rely on `scope: dialog`/`use dialog` for disambiguation on mobile — give the element inside the dialog its own unique accessibility-id/testid instead |
105
+ | `SG-W022` | `Then User see [X] page` under the **mobile** adapter — a native app has no URL, so the step compiles to a bare COMMENT: it reads as an assertion, checks nothing, and the scenario passes whatever is on screen. Worse than a hard failure, because nothing ever goes red. (Its `is on [X] page` twin throws via `route-assertion`; `Given User is on [X] page` is the app-LAUNCH directive and correctly emits nothing) | Assert something actually on the screen — `Then User see [Some Header] text` / a screen-marker accessibility-id — instead of a page/URL check, or tag the scenario `@manual` |
102
106
 
103
107
  ### Runtime error → `Test data "<key>" references ${QA_*} but the environment variable is not set`
104
108