@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,6 +5,7 @@ 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';
|
|
8
|
+
import { unsupportedStepCode } from './patterns/unsupported-step-code';
|
|
8
9
|
import { loadAppProfile, ResolvedAppProfile } from './app-profile';
|
|
9
10
|
import { capabilityRegistry } from '../../capabilities/registry';
|
|
10
11
|
|
|
@@ -280,6 +281,28 @@ export class StepMapper {
|
|
|
280
281
|
this.templateEngine.setBaseContext({ inDialog: false });
|
|
281
282
|
return { code: '', comment: 'Exit dialog scope' };
|
|
282
283
|
}
|
|
284
|
+
// `is on [X] <type>` supports exactly two shapes: `page` (URL from the page-type selector
|
|
285
|
+
// entry) and `dialog` (the scope directive below). Anything else used to fall through to the
|
|
286
|
+
// navigate-to-route pattern, whose draft-era catch-all read every `is on` as a real
|
|
287
|
+
// navigation: Given/When emitted page.goto(<the SCREEN's path> — [X] was never even read),
|
|
288
|
+
// a full reload that wipes the state a multi-step flow just built; Then emitted a URL
|
|
289
|
+
// assertion against that same path, which on a single-URL SPA can never fail. Both were
|
|
290
|
+
// silent, and the one diagnostic that describes this class (SG-W013) is scoped to `page`.
|
|
291
|
+
// Refusing here is the point: "the user is at step X" is a state, not a navigation.
|
|
292
|
+
// `screen` is the mobile analog of `page` — the appium adapter renders it as the app-launch
|
|
293
|
+
// no-op, and a shipped fixture pins that — so it belongs in the supported set.
|
|
294
|
+
const IS_ON_TYPES = ['page', 'screen', 'dialog'];
|
|
295
|
+
if (/\bis\s+on\s+\[/i.test(step.text) && step.elementType &&
|
|
296
|
+
!IS_ON_TYPES.includes(step.elementType)) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`"${step.text}" — \`is on\` names a navigation target, and only \`[X] page\` (\`screen\` on mobile) and ` +
|
|
299
|
+
`\`[X] dialog\` are supported; ` +
|
|
300
|
+
`"${step.elementType}" is neither. If [${step.selectorRef}] is a state inside the current page (a wizard step, ` +
|
|
301
|
+
`a panel), assert it instead: \`Then User see [${step.selectorRef}] section\` (or \`see [X] text with {{v}}\`), ` +
|
|
302
|
+
`and reach that state with the flow's own When steps or an @steps/@extend base scenario. ` +
|
|
303
|
+
`If it really is a separate URL, declare it in selectors.yaml as "type: page" and write \`[${step.selectorRef}] page\`.`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
283
306
|
// Given User is on [...] dialog — set dialog scope (3 cases)
|
|
284
307
|
if (step.text.includes('is on') && step.elementType === 'dialog') {
|
|
285
308
|
this.inDialogScope = true;
|
|
@@ -372,30 +395,42 @@ export class StepMapper {
|
|
|
372
395
|
}
|
|
373
396
|
this.checkDataRefConsumed(step, mappedStep);
|
|
374
397
|
this.checkPageAssertionNoOpOnMobile(step, mappedStep);
|
|
398
|
+
this.checkSetupNoOpOnMobile(step, mappedStep);
|
|
375
399
|
this.checkPageAssertionFallback(step, mappedStep);
|
|
376
400
|
this.checkDeclaredUrlHasNoQuery(step, mappedStep);
|
|
401
|
+
const swallowed = this.checkAssertionClauseSwallowed(step, mappedStep);
|
|
402
|
+
if (swallowed) return swallowed;
|
|
377
403
|
return mappedStep;
|
|
378
404
|
}
|
|
379
405
|
|
|
380
|
-
// No pattern matched
|
|
406
|
+
// No pattern matched. The step is, by definition, vocabulary sungen does not have — so it
|
|
407
|
+
// compiles to a loud `throw`, not a comment. A comment let the scenario go GREEN having asserted
|
|
408
|
+
// nothing, which is the silent-loss class this channel exists to close; SG-E020/SG-W020/SG-E021
|
|
409
|
+
// already treat their own unsupported steps this way and this is the same fact about the step.
|
|
410
|
+
// `@manual` remains the way to park a check sungen genuinely cannot express — it is skipped
|
|
411
|
+
// before mapping, so it never reaches here.
|
|
412
|
+
let reason: string;
|
|
381
413
|
if (/\bexpect\b.*\b(?:local|session)\s+storage\b/i.test(step.text)) {
|
|
382
414
|
// The natural first guess for a storage check — but `expect` steps only read
|
|
383
415
|
// {{response}} refs, so this shape silently compiles to nothing. Point at the
|
|
384
416
|
// supported grammar instead of emitting the generic no-pattern warning.
|
|
385
|
-
this
|
|
386
|
-
|
|
417
|
+
// Kept deliberately more specific than the generic reason below: this shape is the natural
|
|
418
|
+
// first guess for a storage check, so it earns a pointer to the real grammar.
|
|
419
|
+
reason = 'storage assertions use `User see [KEY] in local storage exists` (also: does not exist / is not empty / has length {{n}} / key matching "PATTERN") — the `expect …` form matches no pattern';
|
|
420
|
+
this.diagnostics?.add('SG-W011', reason, {
|
|
387
421
|
feature: this.featureName,
|
|
388
422
|
step: step.text,
|
|
389
423
|
});
|
|
390
424
|
} else {
|
|
391
|
-
|
|
425
|
+
reason = `step "${step.text}" matched no pattern — sungen has no step form for it. Rephrase using a supported step form (see the sungen-gherkin-syntax reference), or tag the scenario @manual if the check cannot be automated.`;
|
|
426
|
+
this.diagnostics?.add('SG-W001', reason, {
|
|
392
427
|
feature: this.featureName,
|
|
393
428
|
step: step.text,
|
|
394
429
|
});
|
|
395
430
|
}
|
|
396
431
|
return {
|
|
397
|
-
code:
|
|
398
|
-
comment: `Unrecognized step pattern: ${step.text}`,
|
|
432
|
+
code: unsupportedStepCode(reason),
|
|
433
|
+
comment: `Unrecognized step pattern — see SG-W001/SG-W011: ${step.text}`,
|
|
399
434
|
};
|
|
400
435
|
}
|
|
401
436
|
|
|
@@ -500,6 +535,64 @@ export class StepMapper {
|
|
|
500
535
|
* disappears from the output (e.g. issue #464). Warning-only: loose containment
|
|
501
536
|
* checks err on the side of NOT flagging.
|
|
502
537
|
*/
|
|
538
|
+
/**
|
|
539
|
+
* SG-E023 — an assertion CLAUSE was swallowed and the step became a bare visibility check.
|
|
540
|
+
*
|
|
541
|
+
* The sibling of SG-E021, for the clauses that are not states. SG-E021 closed the state case
|
|
542
|
+
* declaratively — a pattern lists the states it accounts for and anything else throws — but a
|
|
543
|
+
* dropped `matches` / `contains` / `has text` still slid through: reproduced live, a `matches
|
|
544
|
+
* "<format>"` step compiled by a sungen predating that step lost the assertion entirely, emitted
|
|
545
|
+
* no warning, and left a `toBeVisible()` that passed whatever the label said.
|
|
546
|
+
*
|
|
547
|
+
* So: if the code is a bare visibility assertion while the text still carries a clause that
|
|
548
|
+
* means something STRONGER, compile a throw instead (the SG-W001/SG-E020 treatment). A red test
|
|
549
|
+
* naming the swallowed clause costs minutes; a green one that asserts nothing costs a release.
|
|
550
|
+
*
|
|
551
|
+
* Deliberately narrow — it fires only on the bare-visibility shape, so a pattern that genuinely
|
|
552
|
+
* consumed the clause (`is-hidden`, `contains`, `matches-format`, the state family) is untouched.
|
|
553
|
+
*/
|
|
554
|
+
private checkAssertionClauseSwallowed(step: ParsedStep, mapped: MappedStep): MappedStep | null {
|
|
555
|
+
const code = mapped.code || '';
|
|
556
|
+
// Bare visibility: nothing but a visible/displayed assertion on one locator.
|
|
557
|
+
// A NEGATED visibility assert is a real state assertion, not a bare one — mobile spells
|
|
558
|
+
// `is hidden` as `.not.toBeDisplayed()`, which carries no `toBeHidden` token to exclude on.
|
|
559
|
+
// A golden fixture caught this: without the `.not.` exclusion the guard refused a correct step.
|
|
560
|
+
const isBareVisibility = /toBeVisible\(\)|toBeDisplayed\(\)|__assertVisible\(/.test(code)
|
|
561
|
+
&& !/\.not\./.test(code)
|
|
562
|
+
// `.filter(` counts as consumed: `contains` on a list member compiles to
|
|
563
|
+
// `filter({ hasText: … }) … toBeVisible()`, where the clause shaped the LOCATOR rather than
|
|
564
|
+
// the matcher. Without this the guard refused that legitimate form — a sibling test on main
|
|
565
|
+
// caught it during the merge.
|
|
566
|
+
&& !/\.filter\(/.test(code)
|
|
567
|
+
&& !/toHaveText|toContainText|toBeHidden|toBeDisabled|toBeChecked|RegExp|\/\^|getAttribute|testData\./.test(code);
|
|
568
|
+
if (!isBareVisibility) return null;
|
|
569
|
+
|
|
570
|
+
// Only the clauses no other channel owns. Deliberately NOT `is <state>`: SG-E021 already
|
|
571
|
+
// covers that, and better — a pattern DECLARES the states it accounts for (`StepPattern.states`)
|
|
572
|
+
// and anything else throws, which is checkable at the pattern layer instead of sniffed from the
|
|
573
|
+
// emitted code here. `with {{v}}` belongs to SG-W002 for the same reason.
|
|
574
|
+
const CLAUSES: Array<[RegExp, string]> = [
|
|
575
|
+
// Bare `\bmatches\b`, not `matches "`: the quote-less phrasing is the likelier authoring
|
|
576
|
+
// slip AND what an older compiler leaves behind, and both must be caught.
|
|
577
|
+
[/\bmatches\b/i, 'matches "<format>"'],
|
|
578
|
+
[/\bcontains\b/i, 'contains'],
|
|
579
|
+
[/\b(?:should\s+have|has)\s+text\b/i, 'has text'],
|
|
580
|
+
];
|
|
581
|
+
const hit = CLAUSES.find(([re]) => re.test(step.text));
|
|
582
|
+
if (!hit) return null;
|
|
583
|
+
|
|
584
|
+
const reason = `step "${step.text}" carries the clause \`${hit[1]}\` but compiled to a bare `
|
|
585
|
+
+ `visibility check — the clause was dropped, so the assertion would pass whatever the element `
|
|
586
|
+
+ `says. Either the phrasing is not the supported one for that clause (see the `
|
|
587
|
+
+ `sungen-gherkin-syntax reference), or this sungen predates the step: check you are running `
|
|
588
|
+
+ `the project-local compiler (\`npx sungen\`), not an older global one.`;
|
|
589
|
+
this.diagnostics?.add('SG-E023', reason, { feature: this.featureName, step: step.text });
|
|
590
|
+
return {
|
|
591
|
+
code: unsupportedStepCode(reason),
|
|
592
|
+
comment: `Assertion clause dropped — see SG-E023: ${step.text}`,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
503
596
|
private checkDataRefConsumed(step: ParsedStep, mapped: MappedStep): void {
|
|
504
597
|
if (!step.dataRef || !this.diagnostics) return;
|
|
505
598
|
const code = mapped.code || '';
|
|
@@ -572,6 +665,56 @@ export class StepMapper {
|
|
|
572
665
|
/**
|
|
573
666
|
* Create pattern context for generators
|
|
574
667
|
*/
|
|
668
|
+
/**
|
|
669
|
+
* SG-W023 — a mobile SETUP step that emitted no executable code, so a declared precondition is
|
|
670
|
+
* never actually established.
|
|
671
|
+
*
|
|
672
|
+
* `Given the user is logged out` matches `user-not-logged-in` → the `clear-auth` template, which on
|
|
673
|
+
* Appium is a bare comment: mobile auth lives in the keychain and prefs, not in cookies, so there
|
|
674
|
+
* is nothing a per-step action can clear. The step therefore READS as establishing a precondition
|
|
675
|
+
* and establishes nothing — the scenario runs against whatever the previous one left signed in.
|
|
676
|
+
* Same false-pass class as SG-W022, reached through setup instead of assertion.
|
|
677
|
+
*
|
|
678
|
+
* Keyed on the EMITTED CODE, not on a template name (MappedStep does not carry one), so any future
|
|
679
|
+
* setup template that degrades to a comment is caught by the same check. The phrasings mirror the
|
|
680
|
+
* matchers in driver-ui's `setupPatterns`; `application is running` and `user is logged in` are
|
|
681
|
+
* absent on purpose — the first is legitimately empty on mobile (wdio capabilities already launched
|
|
682
|
+
* the app) and the second is a TODO template on BOTH adapters, so neither is a mobile-specific gap.
|
|
683
|
+
*
|
|
684
|
+
* Deliberately NOT compiled to a `throw`, unlike SG-W020/SG-E021: an unestablished precondition
|
|
685
|
+
* weakens the setup rather than inverting an assertion, and throwing would break every mobile suite
|
|
686
|
+
* that already carries one of these steps. The remedy named in the message is a feature-level reset
|
|
687
|
+
* tier, because app state is reset in the `beforeEach` hook — not mid-scenario by a step.
|
|
688
|
+
*/
|
|
689
|
+
private checkSetupNoOpOnMobile(step: ParsedStep, mapped: MappedStep): void {
|
|
690
|
+
if (this.platform !== 'mobile' || !this.diagnostics) return;
|
|
691
|
+
|
|
692
|
+
const text = step.text.toLowerCase();
|
|
693
|
+
// App-state preconditions → a reset tier clears the layers they name. Backend state is a
|
|
694
|
+
// different remedy entirely, so it carries its own message.
|
|
695
|
+
const isAppState = /\blogged out\b|\bnot logged in\b|clean browser state|fresh browser|new browser session/.test(text);
|
|
696
|
+
const isBackendState = /database is empty|database is cleared|clear database/.test(text);
|
|
697
|
+
if (!isAppState && !isBackendState) return;
|
|
698
|
+
|
|
699
|
+
// Strip comments; whatever remains is what actually runs. Block comments too — a template written
|
|
700
|
+
// with `/* … */` would otherwise read as executable code and silence this check exactly when it
|
|
701
|
+
// was needed.
|
|
702
|
+
const executable = (mapped.code || '')
|
|
703
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
704
|
+
.split('\n')
|
|
705
|
+
.map((l) => l.replace(/\/\/.*$/, '').trim())
|
|
706
|
+
.filter(Boolean)
|
|
707
|
+
.join('');
|
|
708
|
+
if (executable) return;
|
|
709
|
+
|
|
710
|
+
const remedy = isBackendState
|
|
711
|
+
? 'backend state is not app state — reset it through a test API/script, or bind a `@query:` precondition, the same way the web adapter has to'
|
|
712
|
+
: 'mobile auth and app storage live in the keychain/prefs, which no per-step action can clear — declare `@reset:data` on the Feature so each scenario starts from a wiped data layer';
|
|
713
|
+
this.diagnostics.add('SG-W023',
|
|
714
|
+
`this setup step compiled to a COMMENT — the precondition it reads as establishing is never established, so the scenario runs against whatever the previous one left behind and passes regardless. ${remedy}`,
|
|
715
|
+
{ feature: this.featureName, step: step.text });
|
|
716
|
+
}
|
|
717
|
+
|
|
575
718
|
/**
|
|
576
719
|
* SG-W022 — a `page` assertion that emitted no executable code on mobile.
|
|
577
720
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Handlebars from 'handlebars';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import type { RenderImportsOptions } from './adapters/adapter-interface';
|
|
4
5
|
|
|
5
6
|
export class TemplateEngine {
|
|
6
7
|
private templates: Map<string, HandlebarsTemplateDelegate> = new Map();
|
|
@@ -38,9 +39,23 @@ export class TemplateEngine {
|
|
|
38
39
|
.replace(/\t/g, '\\t');
|
|
39
40
|
});
|
|
40
41
|
|
|
42
|
+
// Escape a raw string for embedding inside a REGEX LITERAL (/…/). Metacharacters first —
|
|
43
|
+
// that class doubles a backslash, so the control-char passes below cannot be mis-read as
|
|
44
|
+
// escaping an escape. The line terminators are not cosmetic: a regex literal cannot span
|
|
45
|
+
// lines, so a value carrying a newline (a multi-line heading, a Flutter composite label)
|
|
46
|
+
// produced `/^line one<LF>line two$/` — "Unterminated regular expression", and the ENTIRE
|
|
47
|
+
// .spec.ts stopped parsing. escapeQuotes has always escaped \n/\r/\t for its own
|
|
48
|
+
// container; this helper omitting them was an oversight, not a design choice. U+2028/U+2029
|
|
49
|
+
// terminate a regex literal too, though a JS string literal tolerates them.
|
|
41
50
|
Handlebars.registerHelper('escapeRegex', function(text: string) {
|
|
42
51
|
if (text == null) return '';
|
|
43
|
-
return String(text)
|
|
52
|
+
return String(text)
|
|
53
|
+
.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')
|
|
54
|
+
.replace(/\n/g, '\\n')
|
|
55
|
+
.replace(/\r/g, '\\r')
|
|
56
|
+
.replace(/\t/g, '\\t')
|
|
57
|
+
.replace(/\u2028/g, '\\u2028')
|
|
58
|
+
.replace(/\u2029/g, '\\u2029');
|
|
44
59
|
});
|
|
45
60
|
|
|
46
61
|
Handlebars.registerHelper('includes', function(text: string, search: string) {
|
|
@@ -281,8 +296,8 @@ export class TemplateEngine {
|
|
|
281
296
|
this.baseContext = { ...this.persistentContext };
|
|
282
297
|
}
|
|
283
298
|
|
|
284
|
-
renderImports(options?:
|
|
285
|
-
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb, needsApi: options?.needsApi, needsMock: options?.needsMock, urlAssertImports: options?.urlAssertImports });
|
|
299
|
+
renderImports(options?: RenderImportsOptions): string {
|
|
300
|
+
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb, needsApi: options?.needsApi, needsMock: options?.needsMock, urlAssertImports: options?.urlAssertImports, regexEscapeImport: options?.regexEscapeImport });
|
|
286
301
|
}
|
|
287
302
|
|
|
288
303
|
renderTestFile(data: {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format placeholders → a regex source, for `see [T] <type> matches "<format>"`.
|
|
3
|
+
*
|
|
4
|
+
* QA specs state a label's contract as a SHAPE, not a value: "Format: yyyy/mm/dd ~ yyyy/mm/dd",
|
|
5
|
+
* "Format: 残りX個". Neither existing text assertion can express that — an exact-value assert
|
|
6
|
+
* pins data that changes daily (red while the app is right), and `contains` proves almost
|
|
7
|
+
* nothing ("abc ~ def" passes). This translator keeps the spec's own vocabulary in the Gherkin
|
|
8
|
+
* so the step reads like the requirement, and no regex reaches a file QA has to review.
|
|
9
|
+
*
|
|
10
|
+
* Tokens (everything else is a literal, regex-escaped):
|
|
11
|
+
* yyyy → a 4-digit year mm | dd | hh | ss → a zero-padded 2-digit part
|
|
12
|
+
* X → an integer, thousand separators allowed (the app renders prices as "2,800円")
|
|
13
|
+
* * → any run of characters, for a label that carries surrounding content
|
|
14
|
+
*
|
|
15
|
+
* A token only counts when it is not glued to an ASCII letter, so a literal English prefix
|
|
16
|
+
* survives ("Comment" keeps its "mm"; "yyyy/mm/dd" does not). Japanese/symbol literals are
|
|
17
|
+
* unaffected either way.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Longest-first: `yyyy` must win over a bare `yy`-style prefix of another token. */
|
|
21
|
+
const TOKENS: Array<[string, string]> = [
|
|
22
|
+
['yyyy', '\\d{4}'],
|
|
23
|
+
['mm', '\\d{2}'],
|
|
24
|
+
['dd', '\\d{2}'],
|
|
25
|
+
['hh', '\\d{2}'],
|
|
26
|
+
['ss', '\\d{2}'],
|
|
27
|
+
['X', '\\d+(?:,\\d{3})*'],
|
|
28
|
+
['*', '[\\s\\S]*'],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const isAsciiLetter = (ch: string | undefined): boolean => !!ch && /[A-Za-z]/.test(ch);
|
|
32
|
+
const escapeLiteral = (ch: string): string => ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Translate a format string into an ANCHORED regex source (no delimiters, no flags).
|
|
36
|
+
* Anchored because the spec means the whole label: "Format: X件" is not satisfied by "12件です".
|
|
37
|
+
* Use the `*` token where the label legitimately carries more (a fused mobile node).
|
|
38
|
+
*/
|
|
39
|
+
export function formatToRegexSource(format: string): string {
|
|
40
|
+
let out = '';
|
|
41
|
+
let i = 0;
|
|
42
|
+
while (i < format.length) {
|
|
43
|
+
const prev = i > 0 ? format[i - 1] : undefined;
|
|
44
|
+
let matched = false;
|
|
45
|
+
for (const [token, regex] of TOKENS) {
|
|
46
|
+
if (!format.startsWith(token, i)) continue;
|
|
47
|
+
// `*` is punctuation — the letter-adjacency rule protects word-like tokens only.
|
|
48
|
+
if (token !== '*') {
|
|
49
|
+
const next = format[i + token.length];
|
|
50
|
+
if (isAsciiLetter(prev) || isAsciiLetter(next)) continue;
|
|
51
|
+
}
|
|
52
|
+
out += regex;
|
|
53
|
+
i += token.length;
|
|
54
|
+
matched = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (!matched) {
|
|
58
|
+
out += escapeLiteral(format[i]);
|
|
59
|
+
i += 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return `^${out}$`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** True when the string carries at least one token — i.e. it is a FORMAT, not a plain value. */
|
|
66
|
+
export function hasFormatToken(format: string): boolean {
|
|
67
|
+
return formatToRegexSource(format) !== `^${format.split('').map(escapeLiteral).join('')}$`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The same pattern as a JS **regex literal**, ready to paste into generated code.
|
|
72
|
+
*
|
|
73
|
+
* Not a string: `new RegExp('^\d{4}$')` is a trap — in a single-quoted JS literal `\d` collapses
|
|
74
|
+
* to `d`, so the compiled test would silently match the letter instead of a digit. A regex literal
|
|
75
|
+
* carries the backslashes verbatim; only `/` needs escaping, and date formats are full of them.
|
|
76
|
+
*/
|
|
77
|
+
export function formatToRegexLiteral(format: string): string {
|
|
78
|
+
return `/${formatToRegexSource(format).replace(/\//g, '\\/')}/`;
|
|
79
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal left-to-right lexer that segments generated JS into the four contexts a
|
|
3
|
+
* post-processor must tell apart: string/template literals, regex literals,
|
|
4
|
+
* comments, and everything else.
|
|
5
|
+
*
|
|
6
|
+
* WHY this exists instead of a regex pass per context: independent global regexes
|
|
7
|
+
* have no idea where a literal actually STARTS. `String.replace` retries at every
|
|
8
|
+
* offset, so a pattern that wants "quote … marker … quote" happily pairs the
|
|
9
|
+
* CLOSING quote of one literal with the OPENING quote of the next and rewrites the
|
|
10
|
+
* code in between. Field report (3.2.23, every version 2.0.0+):
|
|
11
|
+
*
|
|
12
|
+
* await page.getByText('Tìm thấy').filter({ hasText: /^§marker§$/ }).waitFor({ state: 'visible' });
|
|
13
|
+
*
|
|
14
|
+
* The marker sits in a REGEX literal, but the string-literal pass matched from the
|
|
15
|
+
* `'` after `thấy` to the `'` before `visible` and emitted
|
|
16
|
+
* `getByText('Tìm thấy`)` … `state: `visible' })` — the whole .spec.ts stopped
|
|
17
|
+
* parsing. Scanning once, in order, makes that class of fault unrepresentable: a
|
|
18
|
+
* quote can only ever open a literal when the scanner is not already inside one.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT a full JS parser — it only needs to find literal boundaries:
|
|
21
|
+
* - `${…}` inside a template literal is body text, not nested code (matches what
|
|
22
|
+
* the previous passes did, and generated templates never nest a literal there).
|
|
23
|
+
* - A `'`/`"` literal must close on its own line; an unterminated one is treated
|
|
24
|
+
* as ordinary code so a stray apostrophe (prose in a comment) cannot desync the
|
|
25
|
+
* rest of the file. Backticks may span lines, but an unterminated one to EOF is
|
|
26
|
+
* likewise demoted to code.
|
|
27
|
+
* - A `/` opens a regex only in expression position and only when it closes on the
|
|
28
|
+
* same line. Both guards keep a division (or a stray slash) from swallowing the
|
|
29
|
+
* literals after it.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type SegmentKind = 'code' | 'string' | 'regex' | 'comment';
|
|
33
|
+
|
|
34
|
+
export interface Segment {
|
|
35
|
+
kind: SegmentKind;
|
|
36
|
+
/** Verbatim source slice, INCLUDING delimiters (quotes, slashes, `//`). */
|
|
37
|
+
text: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Chars that, immediately before a `/`, make it division or a member access — never a regex. */
|
|
41
|
+
const NOT_REGEX_PREFIX = /[\w$)\]]/;
|
|
42
|
+
const REGEX_FLAGS = /[dgimsuvy]/;
|
|
43
|
+
|
|
44
|
+
/** Index just past the closing quote, or -1 when the literal never closes (see module note). */
|
|
45
|
+
function scanQuoted(code: string, start: number, quote: string): number {
|
|
46
|
+
const spansLines = quote === '`';
|
|
47
|
+
for (let i = start + 1; i < code.length; i++) {
|
|
48
|
+
const c = code[i];
|
|
49
|
+
if (c === '\\') { i++; continue; }
|
|
50
|
+
if (c === quote) return i + 1;
|
|
51
|
+
if (c === '\n' && !spansLines) return -1;
|
|
52
|
+
}
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Index just past the regex's flags, or -1 when it is not a single-line regex literal. */
|
|
57
|
+
function scanRegex(code: string, start: number): number {
|
|
58
|
+
// Expression-position guard: skip back over spaces/tabs to the last significant char.
|
|
59
|
+
let p = start - 1;
|
|
60
|
+
while (p >= 0 && (code[p] === ' ' || code[p] === '\t')) p--;
|
|
61
|
+
if (p >= 0 && NOT_REGEX_PREFIX.test(code[p])) return -1;
|
|
62
|
+
|
|
63
|
+
let inClass = false;
|
|
64
|
+
for (let i = start + 1; i < code.length; i++) {
|
|
65
|
+
const c = code[i];
|
|
66
|
+
if (c === '\n') return -1;
|
|
67
|
+
if (c === '\\') { i++; continue; }
|
|
68
|
+
if (inClass) { if (c === ']') inClass = false; continue; }
|
|
69
|
+
if (c === '[') { inClass = true; continue; }
|
|
70
|
+
if (c === '/') {
|
|
71
|
+
let j = i + 1;
|
|
72
|
+
while (j < code.length && REGEX_FLAGS.test(code[j])) j++;
|
|
73
|
+
return j;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Split `code` into ordered segments. Concatenating every `text` reproduces the
|
|
81
|
+
* input byte for byte — callers rewrite only the segments they care about.
|
|
82
|
+
*/
|
|
83
|
+
export function scanSegments(code: string): Segment[] {
|
|
84
|
+
const out: Segment[] = [];
|
|
85
|
+
let codeStart = 0;
|
|
86
|
+
let i = 0;
|
|
87
|
+
|
|
88
|
+
const flushCode = (end: number): void => {
|
|
89
|
+
if (end > codeStart) out.push({ kind: 'code', text: code.slice(codeStart, end) });
|
|
90
|
+
};
|
|
91
|
+
const take = (kind: SegmentKind, end: number): void => {
|
|
92
|
+
flushCode(i);
|
|
93
|
+
out.push({ kind, text: code.slice(i, end) });
|
|
94
|
+
i = codeStart = end;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
while (i < code.length) {
|
|
98
|
+
const c = code[i];
|
|
99
|
+
|
|
100
|
+
// Comments first: a `//` or `/*` reached in code position can never be a regex,
|
|
101
|
+
// and consuming them keeps an apostrophe in prose from opening a literal.
|
|
102
|
+
if (c === '/' && code[i + 1] === '/') {
|
|
103
|
+
const nl = code.indexOf('\n', i);
|
|
104
|
+
take('comment', nl === -1 ? code.length : nl);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (c === '/' && code[i + 1] === '*') {
|
|
108
|
+
const close = code.indexOf('*/', i + 2);
|
|
109
|
+
take('comment', close === -1 ? code.length : close + 2);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (c === '\'' || c === '"' || c === '`') {
|
|
114
|
+
const end = scanQuoted(code, i, c);
|
|
115
|
+
if (end !== -1) { take('string', end); continue; }
|
|
116
|
+
i++; // unterminated → ordinary code
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (c === '/') {
|
|
121
|
+
const end = scanRegex(code, i);
|
|
122
|
+
if (end !== -1) { take('regex', end); continue; }
|
|
123
|
+
i++; // division or stray slash → ordinary code
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
flushCode(code.length);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mask the CONTENT of selector refs `[...]` and data placeholders `{{...}}` before verb matching,
|
|
3
|
+
* keeping the delimiters. A step's verb/keywords come from the SENTENCE — never from a selector or
|
|
4
|
+
* variable NAME — but matchers scan `step.text` with substring checks (e.g. `includes('click')`),
|
|
5
|
+
* so a data ref like `{{double_click_guard_account.email}}` would spuriously satisfy the click
|
|
6
|
+
* matcher and pre-empt fill. The content is replaced with an inert placeholder (`~` — carries no
|
|
7
|
+
* verb/keyword/digit substring) rather than emptied, so structural matchers that require a NON-EMPTY
|
|
8
|
+
* ref survive too — e.g. `expect {{a}} is {{b}}` matches `\{\{[^}]+\}\}`, and `in [X] table` /
|
|
9
|
+
* `/\bto\s+\[/` keep their delimiters. The resolver/generator always receives the ORIGINAL step, so
|
|
10
|
+
* value extraction and codegen are unaffected.
|
|
11
|
+
*
|
|
12
|
+
* Lives in `utils/` rather than in `patterns/index.ts` so the Gherkin parser can reuse it for
|
|
13
|
+
* state-clause extraction (SG-E021) without importing the pattern registry — which would pull the
|
|
14
|
+
* whole capability graph into the parser and close a require cycle. `patterns/index.ts` re-exports
|
|
15
|
+
* it, so its historical import path still resolves.
|
|
16
|
+
*/
|
|
17
|
+
export function maskRefsForMatching(text: string): string {
|
|
18
|
+
return text
|
|
19
|
+
.replace(/\{\{[^}]*\}\}/g, '{{~}}')
|
|
20
|
+
.replace(/\[[^\]]*\]/g, '[~]');
|
|
21
|
+
}
|