@sun-asterisk/sungen 3.2.22-beta.1 → 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.
- package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
- package/dist/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +29 -8
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +20 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +19 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +45 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +150 -19
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +36 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +33 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +78 -24
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +61 -12
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -1
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +104 -29
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +104 -26
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +18 -5
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +19 -5
- package/package.json +3 -3
- package/src/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
- package/src/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
- package/src/generators/test-generator/code-generator.ts +34 -9
- package/src/generators/test-generator/diagnostics.ts +20 -1
- package/src/generators/test-generator/patterns/index.ts +165 -24
- package/src/generators/test-generator/patterns/types.ts +35 -0
- package/src/generators/test-generator/step-mapper.ts +84 -23
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +61 -12
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -1
- package/src/orchestrator/templates/specs-api.ts +104 -26
- 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)
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
comment: resolved.comment,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
188
|
+
if (pattern.generator) {
|
|
189
|
+
return pattern.generator(step, context);
|
|
190
|
+
}
|
|
103
191
|
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
541
|
-
|
|
542
|
-
|
|
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
|
|
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
|
|