@sun-asterisk/sungen 3.2.24 → 3.2.25
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/capabilities/context-router.d.ts.map +1 -1
- package/dist/capabilities/context-router.js +15 -7
- package/dist/capabilities/context-router.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +6 -48
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/changed.d.ts +14 -0
- package/dist/cli/commands/changed.d.ts.map +1 -0
- package/dist/cli/commands/changed.js +128 -0
- package/dist/cli/commands/changed.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +25 -1
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.js +1 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts +19 -0
- package/dist/cli/commands/results-merge.d.ts.map +1 -0
- package/dist/cli/commands/results-merge.js +191 -0
- package/dist/cli/commands/results-merge.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +100 -12
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
- package/dist/exporters/api-catalog-loader.js +10 -3
- package/dist/exporters/api-catalog-loader.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +9 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +55 -3
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +10 -1
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +6 -1
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +10 -0
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.js +1 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +5 -1
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +4 -1
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +7 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +17 -2
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +68 -8
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/results-merge.d.ts +46 -0
- package/dist/exporters/results-merge.d.ts.map +1 -0
- package/dist/exporters/results-merge.js +148 -0
- package/dist/exporters/results-merge.js.map +1 -0
- package/dist/exporters/selector-key-resolver.d.ts +4 -0
- package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
- package/dist/exporters/selector-key-resolver.js +101 -1
- package/dist/exporters/selector-key-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +8 -1
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +5 -4
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +2 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +52 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/dist/generators/test-generator/code-generator.d.ts +6 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +80 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +60 -6
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +59 -5
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +19 -12
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +58 -38
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +23 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
- package/dist/generators/test-generator/recipe-expander.js +9 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- package/dist/generators/test-generator/reset-tier.d.ts +68 -0
- package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
- package/dist/generators/test-generator/reset-tier.js +94 -0
- package/dist/generators/test-generator/reset-tier.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +39 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +144 -5
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +2 -10
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +16 -2
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
- package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/format-pattern.js +82 -0
- package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +20 -1
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
- package/dist/harness/annotation-overrides.d.ts +0 -12
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +9 -5
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/audit.js +4 -4
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +8 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/capability.d.ts +11 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +8 -0
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +5 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +5 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/run-manifest.d.ts +58 -0
- package/dist/harness/run-manifest.d.ts.map +1 -0
- package/dist/harness/run-manifest.js +264 -0
- package/dist/harness/run-manifest.js.map +1 -0
- package/dist/harness/viewpoint-ledger.d.ts +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +25 -4
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/lockstep-version.d.ts +22 -0
- package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
- package/dist/orchestrator/lockstep-version.js +58 -0
- package/dist/orchestrator/lockstep-version.js.map +1 -0
- package/dist/orchestrator/project-deps-sync.d.ts +39 -0
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
- package/dist/orchestrator/project-deps-sync.js +175 -0
- package/dist/orchestrator/project-deps-sync.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +5 -2
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/dist/orchestrator/templates/env.appium.example +10 -0
- package/dist/orchestrator/templates/specs-api.d.ts +4 -2
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +233 -10
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +214 -11
- package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
- package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
- package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/dist/orchestrator/templates/specs-test-data.ts +53 -6
- package/dist/orchestrator/templates/wdio.conf.ts +28 -9
- package/package.json +8 -6
- package/src/capabilities/context-router.ts +13 -5
- package/src/cli/commands/capability.ts +4 -38
- package/src/cli/commands/changed.ts +104 -0
- package/src/cli/commands/delivery.ts +27 -2
- package/src/cli/commands/generate.ts +1 -1
- package/src/cli/commands/results-merge.ts +153 -0
- package/src/cli/commands/update.ts +72 -13
- package/src/cli/index.ts +4 -0
- package/src/exporters/api-catalog-loader.ts +10 -2
- package/src/exporters/api-testcase-formatter.ts +59 -3
- package/src/exporters/csv-exporter.ts +11 -2
- package/src/exporters/json-exporter.ts +7 -2
- package/src/exporters/matrix/build.ts +9 -0
- package/src/exporters/matrix/export.ts +1 -1
- package/src/exporters/matrix/render-csv.ts +5 -1
- package/src/exporters/matrix/render-xlsx.ts +4 -1
- package/src/exporters/matrix/types.ts +7 -0
- package/src/exporters/playwright-report-parser.ts +68 -9
- package/src/exporters/results-merge.ts +150 -0
- package/src/exporters/selector-key-resolver.ts +81 -1
- package/src/exporters/types.ts +8 -1
- package/src/exporters/xlsx-report-builder.ts +5 -4
- package/src/generators/gherkin-parser/index.ts +55 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/src/generators/test-generator/code-generator.ts +86 -7
- package/src/generators/test-generator/diagnostics.ts +60 -6
- package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
- package/src/generators/test-generator/patterns/index.ts +58 -33
- package/src/generators/test-generator/patterns/types.ts +36 -0
- package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
- package/src/generators/test-generator/recipe-expander.ts +9 -1
- package/src/generators/test-generator/reset-tier.ts +93 -0
- package/src/generators/test-generator/step-mapper.ts +149 -6
- package/src/generators/test-generator/template-engine.ts +18 -3
- package/src/generators/test-generator/utils/format-pattern.ts +79 -0
- package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
- package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
- package/src/generators/test-generator/utils/selector-resolver.ts +22 -1
- package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
- package/src/harness/annotation-overrides.ts +10 -5
- package/src/harness/audit.ts +4 -4
- package/src/harness/capability-plan.ts +10 -5
- package/src/harness/capability.ts +16 -0
- package/src/harness/catalog/drivers.yaml +5 -0
- package/src/harness/parse.ts +5 -1
- package/src/harness/run-manifest.ts +254 -0
- package/src/harness/viewpoint-ledger.ts +26 -5
- package/src/index.ts +1 -0
- package/src/orchestrator/lockstep-version.ts +46 -0
- package/src/orchestrator/project-deps-sync.ts +175 -0
- package/src/orchestrator/project-initializer.ts +5 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/src/orchestrator/templates/env.appium.example +10 -0
- package/src/orchestrator/templates/specs-api.ts +214 -11
- package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/src/orchestrator/templates/specs-test-data.ts +53 -6
- package/src/orchestrator/templates/wdio.conf.ts +28 -9
|
@@ -5,10 +5,12 @@ import { StepMapper } from './step-mapper';
|
|
|
5
5
|
import { TestGeneratorAdapter, adapterRegistry } from './adapters';
|
|
6
6
|
import { transformToRuntimeData } from './utils/runtime-data-transformer';
|
|
7
7
|
import { DiagnosticsCollector } from './diagnostics';
|
|
8
|
+
import { parseResetTier, DEFAULT_RESET_TIER } from './reset-tier';
|
|
8
9
|
import { capabilityRegistry } from '../../capabilities/registry';
|
|
9
10
|
import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
|
|
10
11
|
import { readCapabilities } from '../../harness/capability';
|
|
11
12
|
import { parseManualComments } from '../../exporters/scenario-merger';
|
|
13
|
+
import { findSyntaxError } from './utils/spec-syntax-check';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Filter base scenario steps for @extend: only keep Given→When steps.
|
|
@@ -80,7 +82,7 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
|
|
|
80
82
|
const FUNCTIONAL_TAG_PREFIXES = [
|
|
81
83
|
'@parallel', '@serial', '@cleanup:', '@auth:', '@manual', '@no-auth',
|
|
82
84
|
'@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
|
|
83
|
-
'@flow', '@cases:', '@deferred:', '@owned-by:', '@platform:',
|
|
85
|
+
'@flow', '@cases:', '@deferred:', '@owned-by:', '@platform:', '@reset:',
|
|
84
86
|
];
|
|
85
87
|
|
|
86
88
|
function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string[] {
|
|
@@ -328,7 +330,16 @@ export class CodeGenerator {
|
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
// Generate test code (async now to support AI mapping)
|
|
331
|
-
|
|
333
|
+
let testCode = await this.generateTestCode(feature);
|
|
334
|
+
|
|
335
|
+
// Runtime data: replace __SUNGEN_TD$…$__ markers with testData.get() calls.
|
|
336
|
+
// Runs BEFORE the imports are rendered so they can reflect what the transformed
|
|
337
|
+
// body actually references (a marker inside a regex literal becomes an
|
|
338
|
+
// escapeRegExp(...) call that needs its helper imported). Imports carry no
|
|
339
|
+
// markers, so transforming only the body loses nothing.
|
|
340
|
+
if (this.options.runtimeData) {
|
|
341
|
+
testCode = transformToRuntimeData(testCode);
|
|
342
|
+
}
|
|
332
343
|
|
|
333
344
|
// `url-assert` is imported from what the body actually emitted, not from an active capability:
|
|
334
345
|
// page assertions belong to the DEFAULT ui capability, so a capability flag would import the
|
|
@@ -338,16 +349,25 @@ export class CodeGenerator {
|
|
|
338
349
|
const urlAssertImports = ['urlMatches', 'joinPath'].filter((n) => testCode.includes(`${n}(`)).join(', ');
|
|
339
350
|
if (urlAssertImports) this.syncGeneratedHelper(outputDir, 'url-assert.ts', 'specs-url-assert.ts');
|
|
340
351
|
|
|
341
|
-
|
|
352
|
+
// Same body-first rule for the runtime regex-escape helper: only a spec whose
|
|
353
|
+
// transformed body wraps a value in escapeRegExp() imports (and syncs) it.
|
|
354
|
+
const regexEscapeImport = /\bescapeRegExp\(/.test(testCode);
|
|
355
|
+
if (regexEscapeImport) this.syncGeneratedHelper(outputDir, 'regex-escape.ts', 'specs-regex-escape.ts');
|
|
356
|
+
|
|
357
|
+
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi, needsMock, urlAssertImports, regexEscapeImport });
|
|
342
358
|
|
|
343
359
|
// Combine and collapse any runs of 3+ newlines down to 2 (one blank line max)
|
|
344
360
|
const raw = `${imports}\n\n${testCode}`;
|
|
345
361
|
let code = raw.replace(/\n{3,}/g, '\n\n');
|
|
346
362
|
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
363
|
+
// SG-E022 — compiler self-check. Every template and post-processor above assembles
|
|
364
|
+
// TEXT, so a delimiter bug in any of them yields a file Playwright cannot parse at all:
|
|
365
|
+
// one SyntaxError, zero scenarios run. That is exactly how the runtime-data quote-pairing
|
|
366
|
+
// defect reached a user — `sungen generate` reported success and the breakage only
|
|
367
|
+
// surfaced at `npx playwright test`, a step further from the cause. Parsing what we are
|
|
368
|
+
// about to write makes the compiler name its own bad output, at the moment it makes it.
|
|
369
|
+
// (The runtime-data transform itself ran on the body above, before the imports.)
|
|
370
|
+
this.assertParseable(code, feature.name, filePath);
|
|
351
371
|
|
|
352
372
|
return {
|
|
353
373
|
featureName: feature.name,
|
|
@@ -358,6 +378,24 @@ export class CodeGenerator {
|
|
|
358
378
|
};
|
|
359
379
|
}
|
|
360
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Report SG-E022 when the emitted spec is not parseable. Reported, never thrown: the
|
|
383
|
+
* caller still writes the file, so the user keeps the artifact to inspect (and `--strict`
|
|
384
|
+
* turns it into a non-zero exit like every other diagnostic).
|
|
385
|
+
*/
|
|
386
|
+
private assertParseable(code: string, featureName: string, filePath: string): void {
|
|
387
|
+
const detail = findSyntaxError(code);
|
|
388
|
+
if (!detail) return;
|
|
389
|
+
this.diagnostics.add(
|
|
390
|
+
'SG-E022',
|
|
391
|
+
`generated spec "${filePath}" is not parseable TypeScript (${detail}) — Playwright will `
|
|
392
|
+
+ 'fail the whole file with one SyntaxError and run NONE of its scenarios. This is a sungen '
|
|
393
|
+
+ 'compiler bug, not a problem with the .feature: please report it with the step this file '
|
|
394
|
+
+ 'was generated from.',
|
|
395
|
+
{ feature: featureName }
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
361
399
|
/**
|
|
362
400
|
* Count total steps in feature (excluding background).
|
|
363
401
|
* For @extend scenarios, counts base steps + own steps (merged total).
|
|
@@ -767,6 +805,45 @@ export class CodeGenerator {
|
|
|
767
805
|
// package (activity stripped, it isn't needed for terminateApp/activateApp); iosBundleId is the part
|
|
768
806
|
// after `|` when present. Without these the test-file template's `{{#if appPackage}}` stays false and
|
|
769
807
|
// the beforeEach (terminate+activate) reset is never emitted → nav scenarios cascade-fail.
|
|
808
|
+
// Mobile: how much state each scenario starts from — `@reset:none|soft|data|full`, default 'soft'
|
|
809
|
+
// (the terminate+activate relaunch this adapter always did, so an untagged feature is unchanged).
|
|
810
|
+
// Appium-only: web features get a clean slate from navigation and carry no tier.
|
|
811
|
+
const resetTier = this.adapter.frameworkName === 'appium'
|
|
812
|
+
? (parseResetTier(feature.tags || []) ?? DEFAULT_RESET_TIER)
|
|
813
|
+
: undefined;
|
|
814
|
+
|
|
815
|
+
// Per-scenario `@reset:` overrides. Keyed on the scenario's BASE name — NOT the rendered it()
|
|
816
|
+
// title, because a @cases scenario concatenates its title at runtime from the row label, so no
|
|
817
|
+
// compile-time key could match it. The generated spec normalizes a title back to a base name and
|
|
818
|
+
// membership-tests each step, so a miss just inherits the feature tier rather than guessing wrong.
|
|
819
|
+
// `cases` marks a scenario whose it() title is BUILT AT RUNTIME (`name + ' — ' + row.__label`).
|
|
820
|
+
// Only those titles may have a row suffix stripped when looking a tier up; applying that strip to
|
|
821
|
+
// every scenario let a name that merely LOOKED like `<base> — <suffix>` resolve to a different
|
|
822
|
+
// scenario's tier.
|
|
823
|
+
const scenarioResetTiers = this.adapter.frameworkName === 'appium'
|
|
824
|
+
? (feature.scenarios || []).reduce<Array<{ name: string; tier: string; cases: boolean }>>((acc, sc) => {
|
|
825
|
+
const tier = parseResetTier(sc.tags || []);
|
|
826
|
+
if (tier) acc.push({ name: sc.name, tier, cases: (sc.tags || []).some(t => t.startsWith('@cases:')) });
|
|
827
|
+
return acc;
|
|
828
|
+
}, [])
|
|
829
|
+
: undefined;
|
|
830
|
+
// SG-W024 — the runtime lookup is keyed on the scenario NAME, so two scenarios sharing a name
|
|
831
|
+
// cannot be told apart. Counted over EVERY scenario, not just the tagged ones: an UNTAGGED
|
|
832
|
+
// scenario sharing a name with a tagged one silently inherits that tier (weakening a
|
|
833
|
+
// `@reset:none` chain, or paying a reinstall it never asked for), and scanning only the tagged
|
|
834
|
+
// subset missed exactly that case.
|
|
835
|
+
if (scenarioResetTiers && scenarioResetTiers.length) {
|
|
836
|
+
const tiered = new Set(scenarioResetTiers.map(t => t.name));
|
|
837
|
+
const counts = new Map<string, number>();
|
|
838
|
+
for (const sc of feature.scenarios || []) counts.set(sc.name, (counts.get(sc.name) || 0) + 1);
|
|
839
|
+
for (const [name, n] of counts) {
|
|
840
|
+
if (n < 2 || !tiered.has(name)) continue;
|
|
841
|
+
this.diagnostics.add('SG-W024',
|
|
842
|
+
`${n} scenarios in this feature are named "${name}" and at least one carries a @reset: tag — the generated lookup is keyed on the scenario NAME, so every one of them resolves to the SAME tier, including any that declared none. Rename them so each is unique.`,
|
|
843
|
+
{ feature: feature.name, step: name });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
770
847
|
let appPackage: string | undefined;
|
|
771
848
|
let iosBundleId: string | undefined;
|
|
772
849
|
if (this.adapter.frameworkName === 'appium' && feature.path) {
|
|
@@ -802,6 +879,8 @@ export class CodeGenerator {
|
|
|
802
879
|
singleAuthRole,
|
|
803
880
|
appPackage,
|
|
804
881
|
iosBundleId,
|
|
882
|
+
resetTier,
|
|
883
|
+
scenarioResetTiers: scenarioResetTiers && scenarioResetTiers.length ? scenarioResetTiers : undefined,
|
|
805
884
|
});
|
|
806
885
|
}
|
|
807
886
|
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* and would otherwise silently discard or invent it" (spec: sungen_app_profile_spec.md §4).
|
|
4
4
|
*
|
|
5
5
|
* Codes:
|
|
6
|
-
* SG-W001 — step matched no pattern
|
|
6
|
+
* SG-W001 — step matched no pattern: sungen has no step form for the sentence. Compiles to a loud
|
|
7
|
+
* `throw` (it emitted a no-op `// TODO` comment until the unsupported-step gate, which
|
|
8
|
+
* let the scenario pass having asserted nothing). `@manual` is the way to park a check
|
|
9
|
+
* sungen cannot express — such scenarios are skipped before mapping and never reach here
|
|
7
10
|
* SG-W002 — step parsed a {{dataRef}} value but the generated code never uses it
|
|
8
11
|
* SG-W003 — `name` filter defaulted from the Gherkin reference on a name-from-content
|
|
9
12
|
* role (status/alert/…) — such a filter can never match; opt out with `name: null`
|
|
@@ -21,8 +24,10 @@
|
|
|
21
24
|
* entry: the label is display-only, so the step checks existence anywhere in the
|
|
22
25
|
* table — an ordering rule would pass even when the row moved
|
|
23
26
|
* SG-W011 — a storage assertion written in `expect … in local/session storage` form:
|
|
24
|
-
* `expect` steps only read `{{response}}` refs, so this
|
|
25
|
-
*
|
|
27
|
+
* `expect` steps only read `{{response}}` refs, so this matches no pattern — the
|
|
28
|
+
* supported grammar is `User see [KEY] in local storage exists/…`. Compiles to a
|
|
29
|
+
* `throw` (it was a silent no-op until the unsupported-step gate); the reason string
|
|
30
|
+
* keeps naming the real grammar rather than falling back to SG-W001's generic text
|
|
26
31
|
* SG-W012 — a mock-install step AFTER a navigation step in the same block: page.route()
|
|
27
32
|
* registered after goto() misses every request fired during page load — move
|
|
28
33
|
* the install before the navigation (or into Background)
|
|
@@ -40,6 +45,14 @@
|
|
|
40
45
|
* "3 cards are shown" gets an assertion that can only pass if the card's text
|
|
41
46
|
* IS the digit. Count semantics exist on row/table ("see [T] table with {{n}}",
|
|
42
47
|
* "see [Ref] row with {{n}}") and lists ("should have count") only
|
|
48
|
+
* SG-E024 — a mobile app-lifecycle step (restart / reinstall) in a feature that declares no
|
|
49
|
+
* `Path:` app id. The templates resolve the app through a helper the test-file only
|
|
50
|
+
* emits when the id is present, so the spec used to compile clean and then die at
|
|
51
|
+
* runtime with a ReferenceError — taking the whole spec, not the step.
|
|
52
|
+
* SG-E023 — the step carries an assertion clause (`matches "…"`, `contains`, `has text`, a state)
|
|
53
|
+
* but the generated code is a BARE VISIBILITY check: the clause was swallowed by the
|
|
54
|
+
* catch-all and the test would pass whatever the element says. Four separate defects
|
|
55
|
+
* have arrived this way, so the step compiles to a runtime throw instead.
|
|
43
56
|
* SG-E020 — the step matched a pattern whose template the ACTIVE adapter does not ship. Before
|
|
44
57
|
* the platform SPI this was an unhandled `Template not found` that killed the WHOLE
|
|
45
58
|
* feature file and named neither the step nor the adapter. The step now compiles to a
|
|
@@ -54,17 +67,58 @@
|
|
|
54
67
|
* every following step resolves page-wide as if the scope had never been written.
|
|
55
68
|
* Silently-ignored scope is the exact defect class this channel exists for — the step
|
|
56
69
|
* LOOKS like it narrows the search and does not
|
|
70
|
+
* SG-E021 — the step carried a trailing state clause (`… is hidden`) that the pattern which matched
|
|
71
|
+
* it does not account for, so the clause would be SILENTLY DROPPED. This is the worst
|
|
72
|
+
* shape in this catalogue: the others weaken an assertion, this one can INVERT it.
|
|
73
|
+
* `see [X] page is hidden` matched `page-assertion` (priority 13) ahead of `is-hidden`
|
|
74
|
+
* (11), the clause vanished, and the step compiled to `toHaveURL(...)` — asserting the
|
|
75
|
+
* page IS displayed, the exact opposite of what it reads as, and passing while doing so.
|
|
76
|
+
* The pattern layer declares what it accounts for via `StepPattern.states`; anything
|
|
77
|
+
* undeclared reports here and compiles to a loud `throw`
|
|
57
78
|
* SG-W022 — a `page` assertion on the mobile adapter that emitted NO executable code. A native app
|
|
58
79
|
* has no URL, so `Then User see [X] page` compiles to a bare comment: the step reads as
|
|
59
80
|
* an assertion, checks nothing, and the scenario PASSES regardless. Worse than the
|
|
60
81
|
* crash class this plan closed — a crash is visible, a false pass is not. Its `is on`
|
|
61
82
|
* twin routes to `route-assertion`, which throws; this half was silent
|
|
83
|
+
* SG-W023 — a mobile SETUP step that emitted NO executable code, so a declared PRECONDITION is never
|
|
84
|
+
* established. `Given the user is logged out` matches `user-not-logged-in` → `clear-auth`,
|
|
85
|
+
* whose Appium template is a bare comment: mobile auth lives in the keychain and prefs,
|
|
86
|
+
* not in cookies, so there is nothing for a per-step action to clear. The scenario then
|
|
87
|
+
* runs against whatever the PREVIOUS scenario left signed in — the same false-pass class
|
|
88
|
+
* as SG-W022, arriving through setup rather than assertion.
|
|
89
|
+
* Unlike SG-W020/SG-E021 this does NOT compile to a `throw`: an unestablished precondition
|
|
90
|
+
* weakens the setup, it does not INVERT an assertion, and throwing would break every
|
|
91
|
+
* mobile suite already carrying one of these steps. The remedy is a feature-level reset
|
|
92
|
+
* tier (`@reset:data`), which is what the message names — app state is reset between
|
|
93
|
+
* scenarios in the `beforeEach` hook, not mid-scenario by a step.
|
|
94
|
+
* SG-W024 — two or more scenarios in one feature share a name and at least ONE carries a `@reset:`
|
|
95
|
+
* tag. The generated per-scenario lookup is keyed on the scenario NAME (a @cases title is
|
|
96
|
+
* built at runtime, so no compile-time key can match the rendered title), so every
|
|
97
|
+
* same-named scenario resolves to the SAME tier — including one that declared none, which
|
|
98
|
+
* then silently runs at its namesake's tier: a `@reset:none` chain broken, or a reinstall
|
|
99
|
+
* nobody asked for. Counted over EVERY scenario rather than the tagged subset, because the
|
|
100
|
+
* untagged twin is the one that gets the wrong tier and has no tag to be counted by.
|
|
101
|
+
* Reported rather than resolved by precedence — two tests genuinely sharing one title
|
|
102
|
+
* cannot be told apart by a name-keyed lookup, so the only honest fix is a rename. Silently
|
|
103
|
+
* picking a winner would be the same silent-discard shape as SG-W021
|
|
104
|
+
* SG-E022 — the generated spec is not parseable JS/TS, so NOTHING in the file runs — Playwright
|
|
105
|
+
* reports one SyntaxError and every scenario in the unit is lost. A compiler self-check
|
|
106
|
+
* (@babel/parser) on the emitted code, added after a field report where the runtime-data
|
|
107
|
+
* marker post-processor mis-paired quote delimiters across two literals and the user only
|
|
108
|
+
* found out at `npx playwright test`. Unlike every other code here this one blames the
|
|
109
|
+
* compiler, not the Gherkin: it should be impossible, and it is reported so a broken
|
|
110
|
+
* build names itself at generate time instead of one step further downstream
|
|
62
111
|
*
|
|
63
|
-
* Diagnostics never
|
|
64
|
-
*
|
|
112
|
+
* Diagnostics never change the EXIT CODE by themselves; `sungen generate --strict` turns any
|
|
113
|
+
* collected diagnostic into a non-zero exit. They are not all inert, though: SG-W001, SG-W011,
|
|
114
|
+
* SG-E020, SG-E021, SG-W020, SG-E023 and SG-E024 additionally compile the offending step to a runtime
|
|
115
|
+
* `throw`, because a step that reads as a check and silently checks nothing is worse than one that
|
|
116
|
+
* fails loudly. (SG-E022 is the one exception to the "blame the Gherkin" rule and throws nothing: the
|
|
117
|
+
* file is still written so the user keeps the artifact to inspect.)
|
|
118
|
+
* Generation always COMPLETES — the rest of the scenario and the rest of the file still compile.
|
|
65
119
|
*/
|
|
66
120
|
|
|
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';
|
|
121
|
+
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-E021' | 'SG-W020' | 'SG-W021' | 'SG-W022' | 'SG-W023' | 'SG-W024' | 'SG-E022' | 'SG-E023' | 'SG-E024';
|
|
68
122
|
|
|
69
123
|
export interface Diagnostic {
|
|
70
124
|
code: DiagnosticCode;
|
|
@@ -25,13 +25,45 @@ const reExpect = new RegExp(`^\\s*(?:User\\s+)?expect\\s+(${VALUE})\\s+is\\s+(no
|
|
|
25
25
|
const reMatchesSchemaMasked = /^\s*(?:User\s+)?expect\s+\{\{~\}\}\s+matches\s+schema\s+\[~\]\s*$/i;
|
|
26
26
|
const reMatchesSchema = new RegExp(`^\\s*(?:User\\s+)?expect\\s+(\\{\\{[^}]+\\}\\})\\s+matches\\s+schema\\s+\\[([A-Za-z0-9_.-]+)\\]\\s*$`, 'i');
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* `{{var}}` EMBEDDED in a quoted literal — test-data keys and paths only
|
|
30
|
+
* (`user.name`, `q.rows[0].id`, `items[*]`, kebab flow namespaces like
|
|
31
|
+
* `password-reset.email`). `{{$...}}` dynamic tokens are excluded on purpose:
|
|
32
|
+
* they have no test-data key, so converting one to a get() call throws
|
|
33
|
+
* "Test data key not found" at runtime — they stay literal.
|
|
34
|
+
*/
|
|
35
|
+
const RE_EMBEDDED_VAR = /\{\{\s*([\w.[\]*-]+?)\s*\}\}/g;
|
|
36
|
+
|
|
37
|
+
/** Escape static literal text for a template-literal context. */
|
|
38
|
+
const escapeForTemplate = (s: string): string =>
|
|
39
|
+
s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Render a quoted literal's BODY as a JS expression. Text without vars keeps
|
|
43
|
+
* its exact JSON.stringify shape; an embedded `{{var}}` interpolates the
|
|
44
|
+
* runtime value (the literal used to ship verbatim, so the assertion compared
|
|
45
|
+
* against the raw `{{...}}` text and could never pass with correct data).
|
|
46
|
+
*/
|
|
47
|
+
export function literalExpr(text: string): string {
|
|
48
|
+
const vars = [...text.matchAll(RE_EMBEDDED_VAR)];
|
|
49
|
+
if (vars.length === 0) return JSON.stringify(text);
|
|
50
|
+
if (vars.length === 1 && vars[0][0] === text) return `testData.get(${JSON.stringify(vars[0][1])})`;
|
|
51
|
+
let out = '';
|
|
52
|
+
let last = 0;
|
|
53
|
+
for (const m of vars) {
|
|
54
|
+
out += escapeForTemplate(text.slice(last, m.index!)) + `\${testData.get(${JSON.stringify(m[1])})}`;
|
|
55
|
+
last = m.index! + m[0].length;
|
|
56
|
+
}
|
|
57
|
+
return `\`${out + escapeForTemplate(text.slice(last))}\``;
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
/** Render a value token (`{{var}}` | "literal" | 'literal' | number) as a JS expression. */
|
|
29
61
|
function valueExpr(token: string): string {
|
|
30
62
|
const t = token.trim();
|
|
31
63
|
const v = t.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
32
64
|
if (v) return `testData.get(${JSON.stringify(v[1])})`;
|
|
33
65
|
const q = t.match(/^["'](.*)["']$/);
|
|
34
|
-
if (q) return
|
|
66
|
+
if (q) return literalExpr(q[1]);
|
|
35
67
|
if (/^-?\d+(?:\.\d+)?$/.test(t)) return t;
|
|
36
68
|
return JSON.stringify(t);
|
|
37
69
|
}
|
|
@@ -46,7 +78,10 @@ export const expectPatterns: StepPattern[] = [
|
|
|
46
78
|
// odd chars) can claim the pattern yet not re-match here — emit a clear TODO instead of crashing.
|
|
47
79
|
const m = step.text.match(reMatchesSchema);
|
|
48
80
|
if (!m) return { code: `// TODO: malformed 'matches schema' step (expected: expect {{name.body}} matches schema [Ref]): ${step.text}`, comment: 'Unrecognized matches-schema step' };
|
|
49
|
-
|
|
81
|
+
// raw(), not get(): Ajv must receive the NATIVE bound object. get() serializes
|
|
82
|
+
// to JSON text, so every schema validation failed with "(root) must be object"
|
|
83
|
+
// no matter how correct the response body was.
|
|
84
|
+
const value = `testData.raw(${JSON.stringify(m[1].replace(/^\{\{\s*|\s*\}\}$/g, ''))})`;
|
|
50
85
|
const ref = m[2];
|
|
51
86
|
const code = `{ const __schema = api.assertSchema(${value}, ${JSON.stringify(ref)}); expect(__schema.ok, \`matches schema [${ref}]: \${__schema.errors}\`).toBe(true); }`;
|
|
52
87
|
return { code, comment: `Expect ${m[1]} matches schema [${ref}]` };
|
|
@@ -3,23 +3,13 @@ import { MappedStep } from '../step-mapper';
|
|
|
3
3
|
import { StepPattern, PatternContext, Platform } from './types';
|
|
4
4
|
import { capabilityRegistry } from '../../../capabilities/registry';
|
|
5
5
|
import { discoverAndRegisterCapabilities } from '../../../capabilities/discover';
|
|
6
|
+
import { maskRefsForMatching } from '../utils/mask-refs-for-matching';
|
|
7
|
+
import { unsupportedStepCode } from './unsupported-step-code';
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* so a data ref like `{{double_click_guard_account.email}}` would spuriously satisfy the click
|
|
12
|
-
* matcher and pre-empt fill. The content is replaced with an inert placeholder (`~` — carries no
|
|
13
|
-
* verb/keyword/digit substring) rather than emptied, so structural matchers that require a NON-EMPTY
|
|
14
|
-
* ref survive too — e.g. `expect {{a}} is {{b}}` matches `\{\{[^}]+\}\}`, and `in [X] table` /
|
|
15
|
-
* `/\bto\s+\[/` keep their delimiters. The resolver/generator always receives the ORIGINAL step, so
|
|
16
|
-
* value extraction and codegen are unaffected.
|
|
17
|
-
*/
|
|
18
|
-
export function maskRefsForMatching(text: string): string {
|
|
19
|
-
return text
|
|
20
|
-
.replace(/\{\{[^}]*\}\}/g, '{{~}}')
|
|
21
|
-
.replace(/\[[^\]]*\]/g, '[~]');
|
|
22
|
-
}
|
|
9
|
+
// Both helpers moved out of this file so the Gherkin parser (state-clause extraction) and the
|
|
10
|
+
// step-mapper (SG-W001 no-pattern fallback) can use them without importing the pattern registry.
|
|
11
|
+
// Re-exported here because their historical import path is `patterns/index.ts`.
|
|
12
|
+
export { maskRefsForMatching, unsupportedStepCode };
|
|
23
13
|
|
|
24
14
|
/**
|
|
25
15
|
* Gesture forms that OWN the word "tap" — each has its own pattern keyed on it (`mobile-double-tap`
|
|
@@ -76,23 +66,6 @@ export function normalizeVerbs(text: string): string {
|
|
|
76
66
|
*/
|
|
77
67
|
const TEMPLATE_NOT_FOUND = /^Template not found: (.+)\.hbs in any subfolder$/;
|
|
78
68
|
|
|
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
|
-
|
|
96
69
|
/**
|
|
97
70
|
* Pattern Registry - manages all step patterns
|
|
98
71
|
*/
|
|
@@ -170,6 +143,16 @@ export class PatternRegistry {
|
|
|
170
143
|
const resolved = pattern.resolver(step, context);
|
|
171
144
|
if (!resolved) continue; // resolver declined — fall through
|
|
172
145
|
|
|
146
|
+
// State-clause gate. Deliberately AFTER the resolver, not before it: a resolver may return
|
|
147
|
+
// null to say "not mine after all", and the search then continues to a lower-priority
|
|
148
|
+
// pattern that may well be the one that asserts the state. Gating earlier judged a pattern
|
|
149
|
+
// that had not yet claimed the step — `row-count` declines a non-numeric `{{value}}` so
|
|
150
|
+
// `see [R] row with {{status_label}} is disabled` should reach see-with-variable-disabled,
|
|
151
|
+
// but the gate short-circuited it into a throw. Only the pattern that actually OWNS the
|
|
152
|
+
// step gets to be judged on what it drops.
|
|
153
|
+
const resolverStateMiss = PatternRegistry.stateMiss(pattern, context, step);
|
|
154
|
+
if (resolverStateMiss) return resolverStateMiss;
|
|
155
|
+
|
|
173
156
|
// Auto-inject parent scoping if step has parentRef
|
|
174
157
|
// Skip for table-* patterns — they resolve the table name internally from step text
|
|
175
158
|
if (step.parentRef && step.parentType && !pattern.name.startsWith('table-')) {
|
|
@@ -186,6 +169,9 @@ export class PatternRegistry {
|
|
|
186
169
|
}
|
|
187
170
|
|
|
188
171
|
if (pattern.generator) {
|
|
172
|
+
// A legacy generator has no decline path, so this pattern owns the step already.
|
|
173
|
+
const generatorStateMiss = PatternRegistry.stateMiss(pattern, context, step);
|
|
174
|
+
if (generatorStateMiss) return generatorStateMiss;
|
|
189
175
|
return pattern.generator(step, context);
|
|
190
176
|
}
|
|
191
177
|
|
|
@@ -228,6 +214,45 @@ export class PatternRegistry {
|
|
|
228
214
|
};
|
|
229
215
|
}
|
|
230
216
|
|
|
217
|
+
/**
|
|
218
|
+
* SG-E021 — the step carries a trailing state clause the matched pattern does not account for.
|
|
219
|
+
*
|
|
220
|
+
* Priority decides which pattern wins, and the winner is not always the one that reads the clause:
|
|
221
|
+
* `see [X] page is hidden` matched `page-assertion` (13) ahead of `is-hidden` (11), so `is hidden`
|
|
222
|
+
* was dropped and the step compiled to `toHaveURL(...)` — asserting the page IS shown, the exact
|
|
223
|
+
* inverse of the sentence, and passing. Nothing detected it, because the clause lived only inside
|
|
224
|
+
* `step.text` and each pattern re-regexed it privately.
|
|
225
|
+
*
|
|
226
|
+
* `states` makes the claim explicit and checkable: a pattern declares what its template accounts
|
|
227
|
+
* for, and anything else lands here. Strict reading — an ABSENT `states` accounts for nothing —
|
|
228
|
+
* because the failure modes are asymmetric. Under-declaring throws on valid input, which is loud,
|
|
229
|
+
* local, and fixed by one annotation. Over-declaring restores the silent inversion this exists to
|
|
230
|
+
* close, and nothing catches it again.
|
|
231
|
+
*/
|
|
232
|
+
private static stateMiss(
|
|
233
|
+
pattern: StepPattern, context: PatternContext, step: ParsedStep
|
|
234
|
+
): MappedStep | null {
|
|
235
|
+
const state = step.state;
|
|
236
|
+
if (!state) return null;
|
|
237
|
+
if (pattern.states?.includes(state)) return null;
|
|
238
|
+
|
|
239
|
+
const reason =
|
|
240
|
+
`step "${step.text}" carries the state clause "is ${state}", but the pattern that matched it ` +
|
|
241
|
+
`("${pattern.name}") does not assert that state — the clause would be SILENTLY DROPPED and the ` +
|
|
242
|
+
`step would check something other than what it reads as. Use a step form that owns the state ` +
|
|
243
|
+
`(e.g. "User see [Ref] <type> is ${state}" on the element itself), or drop the clause if the ` +
|
|
244
|
+
`bare assertion is what you meant. If sungen cannot express the check, tag the scenario @manual.`;
|
|
245
|
+
|
|
246
|
+
context.diagnostics?.add('SG-E021', reason, {
|
|
247
|
+
feature: context.featureName,
|
|
248
|
+
step: step.text,
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
code: unsupportedStepCode(reason),
|
|
252
|
+
comment: `"is ${state}" not asserted by "${pattern.name}" — see SG-E021`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
231
256
|
/**
|
|
232
257
|
* SG-E020 — the pattern is in-platform but the active adapter ships no template for it. A gap in
|
|
233
258
|
* the adapter, not in the authored Gherkin: name every coordinate a reader needs to act on it.
|
|
@@ -19,6 +19,28 @@ export interface DiagnosticsSink {
|
|
|
19
19
|
*/
|
|
20
20
|
export type Platform = 'web' | 'mobile';
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The canonical state clauses an assertion step may carry, exactly as documented for authors in
|
|
24
|
+
* `README.md` ("States") and in the `sungen-gherkin-syntax` skill. Deliberately CLOSED: real feature
|
|
25
|
+
* files carry trailing phrases like `is rejected` / `is active` / `is unchanged` that belong to other
|
|
26
|
+
* grammars or are prose, and reading those as state clauses would put valid steps in front of the
|
|
27
|
+
* SG-E021 gate. Widening this union widens that gate — add a member only alongside a pattern that
|
|
28
|
+
* asserts it.
|
|
29
|
+
*/
|
|
30
|
+
export type StateClause =
|
|
31
|
+
| 'hidden'
|
|
32
|
+
| 'visible'
|
|
33
|
+
| 'disabled'
|
|
34
|
+
| 'enabled'
|
|
35
|
+
| 'checked'
|
|
36
|
+
| 'unchecked'
|
|
37
|
+
| 'focused'
|
|
38
|
+
| 'empty'
|
|
39
|
+
| 'loading'
|
|
40
|
+
| 'selected'
|
|
41
|
+
| 'sorted ascending'
|
|
42
|
+
| 'sorted descending';
|
|
43
|
+
|
|
22
44
|
/**
|
|
23
45
|
* Pattern matcher function or regex
|
|
24
46
|
*/
|
|
@@ -104,4 +126,18 @@ export interface StepPattern {
|
|
|
104
126
|
* emitted when absent.
|
|
105
127
|
*/
|
|
106
128
|
platformAlternative?: string;
|
|
129
|
+
/**
|
|
130
|
+
* State clauses this pattern's generated code ACTUALLY asserts. ABSENT = asserts none.
|
|
131
|
+
*
|
|
132
|
+
* The contract runs in one direction and is easy to get backwards: declaring a state is a promise
|
|
133
|
+
* that the emitted code checks it. A pattern that wins a step carrying an UNDECLARED state clause
|
|
134
|
+
* emits SG-E021 and compiles to a loud `throw`, because the alternative is what this field exists
|
|
135
|
+
* to prevent — the clause being silently dropped and the assertion reading as something it is not.
|
|
136
|
+
* (`see [X] page is hidden` matched `page-assertion` at a higher priority than `is-hidden`, so the
|
|
137
|
+
* clause vanished and the step asserted the OPPOSITE: that the page IS displayed.)
|
|
138
|
+
*
|
|
139
|
+
* So: under-declaring throws on valid input, over-declaring re-opens the silent-drop hole. Declare
|
|
140
|
+
* a state only after reading the template the resolver names.
|
|
141
|
+
*/
|
|
142
|
+
states?: ReadonlyArray<StateClause>;
|
|
107
143
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The step body emitted in place of code sungen cannot honestly produce — because the active adapter
|
|
3
|
+
* ships no template for it (SG-E020), because the pattern is off-platform (SG-W020), because the
|
|
4
|
+
* matched pattern would silently DROP the author's state clause (SG-E021), or because no pattern
|
|
5
|
+
* matched at all (SG-W001/SG-W011). A comment alone would let the test go GREEN having asserted
|
|
6
|
+
* nothing — the silent-loss class these diagnostics exist to close — so the step throws at run time
|
|
7
|
+
* as well as reporting at compile time.
|
|
8
|
+
*
|
|
9
|
+
* `JSON.stringify` does the quoting, NOT a hand-rolled `replace(/'/g, …)`. The reason string embeds
|
|
10
|
+
* arbitrary author text (the step sentence), and hand-escaping only the quote character is wrong for
|
|
11
|
+
* any input containing a backslash or a raw newline: a step ending `…\` would emit `'…\\'`, whose
|
|
12
|
+
* trailing escaped-backslash-then-quote breaks the literal and takes the WHOLE generated spec down
|
|
13
|
+
* with a syntax error — reopening, one level deeper, the exact "one bad step kills the file" failure
|
|
14
|
+
* this function exists to prevent. Step text here is routinely non-ASCII (Vietnamese, Japanese),
|
|
15
|
+
* which JSON.stringify also handles correctly.
|
|
16
|
+
*/
|
|
17
|
+
export function unsupportedStepCode(reason: string): string {
|
|
18
|
+
return `throw new Error(${JSON.stringify(`[sungen] ${reason}`)});`;
|
|
19
|
+
}
|
|
@@ -103,8 +103,16 @@ function expandPrimitive(prim: unknown, ctx: RecipeExpandContext, itemVar: strin
|
|
|
103
103
|
return { partial: 'recipe-click-text', targetExpr: expr };
|
|
104
104
|
}
|
|
105
105
|
case 'expectText': {
|
|
106
|
+
// `exact: true` asserts the element's WHOLE text (toHaveText) instead of a substring
|
|
107
|
+
// (toContainText) — same opt-in flag `clickText` carries. Substring stays the default so
|
|
108
|
+
// existing recipes keep their behavior.
|
|
106
109
|
const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
|
|
107
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
partial: 'recipe-expect-text',
|
|
112
|
+
targetExpr: targetExpr(o.locator ?? '$trigger'),
|
|
113
|
+
valueExpr: valueExpr(o.value ?? '$value'),
|
|
114
|
+
exact: o.exact === true,
|
|
115
|
+
};
|
|
108
116
|
}
|
|
109
117
|
case 'expectValue': {
|
|
110
118
|
const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile reset tiers — how much state a scenario starts from.
|
|
3
|
+
*
|
|
4
|
+
* Web gets a clean slate for free: a URL reload is effectively a fresh state. Mobile does not. To save
|
|
5
|
+
* time, mobile automation keeps the app alive across a whole session, so the end state of one scenario
|
|
6
|
+
* becomes the start state of the next — which breaks test independence three ways: scenarios can no
|
|
7
|
+
* longer run in isolation or in parallel, a "nearly right" leftover state makes the FOLLOWING scenario
|
|
8
|
+
* fail for reasons that have nothing to do with it, and a scenario that dies mid-flow poisons every
|
|
9
|
+
* scenario after it.
|
|
10
|
+
*
|
|
11
|
+
* The reason nobody just reinstalls between scenarios is cost: a cold start is 5–30s, which across a
|
|
12
|
+
* large suite is the whole CI budget. So the state layers (local DB, cache, prefs, keychain, OS
|
|
13
|
+
* permissions) need a LADDER rather than one all-or-nothing switch — the same conclusion every major
|
|
14
|
+
* framework reached (Appium's `mobile: clearApp`, Detox's `device.reloadReactNative()`, the OS-level
|
|
15
|
+
* reset APIs in XCUITest/Espresso).
|
|
16
|
+
*
|
|
17
|
+
* none — nothing is reset. State carries over BY DESIGN (a cart that accumulates across scenarios).
|
|
18
|
+
* soft — terminateApp + activateApp. Clears in-memory state and the navigation stack. THE DEFAULT,
|
|
19
|
+
* and exactly what the compiler emitted before tiers existed, so no existing suite changes.
|
|
20
|
+
* data — soft + wipe the app's data layer (local DB, cache, prefs, keychain).
|
|
21
|
+
* full — remove + reinstall. Also resets permissions and any install-time state.
|
|
22
|
+
*
|
|
23
|
+
* Declared per feature via `@reset:<tier>`; a run can RAISE the floor with SUNGEN_RESET_TIER without
|
|
24
|
+
* regenerating specs (resolved inside the generated spec, the same runtime model as test-data overlays).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Ordered weakest → strongest. Index order IS the comparison, so never reorder this. */
|
|
28
|
+
export const RESET_TIERS = ['none', 'soft', 'data', 'full'] as const;
|
|
29
|
+
|
|
30
|
+
export type ResetTier = (typeof RESET_TIERS)[number];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `soft` — the behaviour the appium adapter emitted before tiers existed. Keeping it the default is
|
|
34
|
+
* what makes this feature additive: an untagged feature compiles to the same spec it did before.
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_RESET_TIER: ResetTier = 'soft';
|
|
37
|
+
|
|
38
|
+
function isResetTier(value: string): value is ResetTier {
|
|
39
|
+
return (RESET_TIERS as readonly string[]).includes(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read the `@reset:<tier>` tag off a tag list.
|
|
44
|
+
*
|
|
45
|
+
* Returns `undefined` when no `@reset:` tag is present — deliberately NOT the default. Callers need to
|
|
46
|
+
* tell "absent" from "explicitly soft": a scenario with no tag must inherit its feature's tier, and
|
|
47
|
+
* collapsing absent into `soft` here would silently RAISE a `@reset:none` feature.
|
|
48
|
+
*
|
|
49
|
+
* An unrecognized value warns and yields the default rather than throwing, matching how `@cleanup:`
|
|
50
|
+
* handles an unknown key — a typo in a tag should not stop a build.
|
|
51
|
+
*/
|
|
52
|
+
export function parseResetTier(tags: string[]): ResetTier | undefined {
|
|
53
|
+
const tag = tags.find((t) => t.startsWith('@reset:'));
|
|
54
|
+
if (!tag) return undefined;
|
|
55
|
+
const value = tag.slice('@reset:'.length).trim().toLowerCase();
|
|
56
|
+
if (isResetTier(value)) return value;
|
|
57
|
+
console.warn(`⚠ Unknown @reset:${value} — valid tiers: ${RESET_TIERS.join(', ')}. Using '${DEFAULT_RESET_TIER}'.`);
|
|
58
|
+
return DEFAULT_RESET_TIER;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The stronger of two tiers. Used to combine a declared tier with a run-level floor. */
|
|
62
|
+
export function maxResetTier(a: ResetTier, b: ResetTier): ResetTier {
|
|
63
|
+
return RESET_TIERS.indexOf(a) >= RESET_TIERS.indexOf(b) ? a : b;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* One rung harder — what the NEXT scenario resets to after this one failed.
|
|
68
|
+
*
|
|
69
|
+
* A failed scenario left unknown state behind (a half-written local DB, an open modal) that a plain
|
|
70
|
+
* process relaunch does not undo, so the scenario after it deserves a stronger reset than it asked for.
|
|
71
|
+
*
|
|
72
|
+
* `none` is a FIXED POINT, on purpose. It is an explicit "state carries over" contract — a chain of
|
|
73
|
+
* scenarios building up one cart. Escalating it would silently break that contract, and the chain is
|
|
74
|
+
* already dead once one link fails, so the relaunch would only change how the rest fail, not whether.
|
|
75
|
+
*
|
|
76
|
+
* Applies to the next DIFFERENT scenario only — never to a RETRY of the one that just failed. The
|
|
77
|
+
* runner re-runs a failed scenario, and giving attempt 2 a stronger reset would break the guarantee
|
|
78
|
+
* `mochaOpts.retries` rests on: race victims recover on attempt 2, real failures fail both attempts.
|
|
79
|
+
* A dirty-state failure IS a real failure; if its retry silently got a wiped data layer it would pass,
|
|
80
|
+
* the reporter keeps the last attempt, and a scenario that only works after a wipe would ship as a
|
|
81
|
+
* clean `Passed`. That false pass is worse than the flakiness escalation was added to fix.
|
|
82
|
+
*
|
|
83
|
+
* The generated spec emits this same ladder inline; both exist so the compiler and the runtime cannot
|
|
84
|
+
* drift apart, and a test asserts they agree.
|
|
85
|
+
*/
|
|
86
|
+
export function escalateResetTier(tier: ResetTier): ResetTier {
|
|
87
|
+
switch (tier) {
|
|
88
|
+
case 'none': return 'none';
|
|
89
|
+
case 'soft': return 'data';
|
|
90
|
+
case 'data': return 'full';
|
|
91
|
+
case 'full': return 'full';
|
|
92
|
+
}
|
|
93
|
+
}
|