@sun-asterisk/sungen 3.2.29 → 3.2.31
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/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +2 -1
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +6 -2
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/inspect.js +1 -1
- package/dist/cli/commands/inspect.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts.map +1 -1
- package/dist/cli/commands/results-merge.js +9 -2
- package/dist/cli/commands/results-merge.js.map +1 -1
- package/dist/cli/commands/results-sanitize.d.ts +24 -0
- package/dist/cli/commands/results-sanitize.d.ts.map +1 -0
- package/dist/cli/commands/results-sanitize.js +126 -0
- package/dist/cli/commands/results-sanitize.js.map +1 -0
- package/dist/cli/commands/script-check.d.ts.map +1 -1
- package/dist/cli/commands/script-check.js +3 -1
- package/dist/cli/commands/script-check.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +3 -0
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +3 -6
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +7 -10
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +4 -2
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/render-coverage-sheet.d.ts.map +1 -1
- package/dist/exporters/matrix/render-coverage-sheet.js +3 -1
- package/dist/exporters/matrix/render-coverage-sheet.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +3 -1
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-overview-sheet.d.ts.map +1 -1
- package/dist/exporters/matrix/render-overview-sheet.js +5 -9
- package/dist/exporters/matrix/render-overview-sheet.js.map +1 -1
- package/dist/exporters/matrix/result-totals.d.ts +23 -0
- package/dist/exporters/matrix/result-totals.d.ts.map +1 -0
- package/dist/exporters/matrix/result-totals.js +21 -0
- package/dist/exporters/matrix/result-totals.js.map +1 -0
- package/dist/exporters/matrix/testcase-summary-band.d.ts.map +1 -1
- package/dist/exporters/matrix/testcase-summary-band.js +2 -6
- package/dist/exporters/matrix/testcase-summary-band.js.map +1 -1
- package/dist/exporters/qa-env-secret-denylist.js +2 -2
- package/dist/exporters/qa-env-secret-denylist.js.map +1 -1
- package/dist/exporters/report-path-sanitizer.d.ts +38 -0
- package/dist/exporters/report-path-sanitizer.d.ts.map +1 -0
- package/dist/exporters/report-path-sanitizer.js +137 -0
- package/dist/exporters/report-path-sanitizer.js.map +1 -0
- package/dist/exporters/result-variants.d.ts.map +1 -1
- package/dist/exporters/result-variants.js +2 -1
- package/dist/exporters/result-variants.js.map +1 -1
- package/dist/exporters/sensitive-key.d.ts +31 -0
- package/dist/exporters/sensitive-key.d.ts.map +1 -0
- package/dist/exporters/sensitive-key.js +50 -0
- package/dist/exporters/sensitive-key.js.map +1 -0
- package/dist/exporters/test-data-resolver.d.ts +4 -1
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +12 -2
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +3 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +17 -9
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/gherkin-parser/parent-scope-clause.d.ts +54 -0
- package/dist/generators/gherkin-parser/parent-scope-clause.d.ts.map +1 -0
- package/dist/generators/gherkin-parser/parent-scope-clause.js +109 -0
- package/dist/generators/gherkin-parser/parent-scope-clause.js.map +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +23 -18
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +4 -19
- 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/after-all.hbs +18 -0
- package/dist/generators/test-generator/adapters/appium/templates/after-each.hbs +18 -0
- package/dist/generators/test-generator/adapters/appium/templates/before-all.hbs +18 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-at-position-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-element-with-text.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/double-click-action.hbs +7 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/frame-enter-action.hbs +3 -6
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/table-paginate-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/long-press-action.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/pinch-zoom-action.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -1
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +54 -23
- package/dist/generators/test-generator/adapters/index.d.ts +1 -1
- package/dist/generators/test-generator/adapters/index.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +4 -19
- 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/after-all.hbs +19 -1
- package/dist/generators/test-generator/adapters/playwright/templates/after-each.hbs +18 -0
- package/dist/generators/test-generator/adapters/playwright/templates/before-all.hbs +19 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/check-action.hbs +6 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-at-position-action.hbs +23 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +8 -8
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +7 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-paginate-action.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/uncheck-action.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/wait-for-page.hbs +18 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +32 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +13 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +2 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +2 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +2 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +14 -3
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +13 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +3 -3
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/checked-opts.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +10 -8
- package/dist/generators/test-generator/code-generator.d.ts +37 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +160 -39
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +79 -5
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +78 -4
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +24 -5
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +92 -14
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +18 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.js +40 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +63 -9
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +166 -28
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +4 -19
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +17 -12
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +7 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +19 -2
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/dialog-scope-identity.d.ts +41 -0
- package/dist/generators/test-generator/utils/dialog-scope-identity.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/dialog-scope-identity.js +61 -0
- package/dist/generators/test-generator/utils/dialog-scope-identity.js.map +1 -0
- package/dist/generators/test-generator/utils/escape-js-string.d.ts +17 -0
- package/dist/generators/test-generator/utils/escape-js-string.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/escape-js-string.js +29 -0
- package/dist/generators/test-generator/utils/escape-js-string.js.map +1 -0
- package/dist/generators/test-generator/utils/nearest-match.d.ts +24 -0
- package/dist/generators/test-generator/utils/nearest-match.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/nearest-match.js +56 -0
- package/dist/generators/test-generator/utils/nearest-match.js.map +1 -0
- package/dist/generators/test-generator/utils/selector-entry-fields.d.ts +58 -0
- package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/selector-entry-fields.js +119 -0
- package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -0
- package/dist/generators/test-generator/utils/selector-entry-walk.d.ts +25 -0
- package/dist/generators/test-generator/utils/selector-entry-walk.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/selector-entry-walk.js +54 -0
- package/dist/generators/test-generator/utils/selector-entry-walk.js.map +1 -0
- package/dist/generators/test-generator/utils/selector-nth-validation.d.ts +59 -0
- package/dist/generators/test-generator/utils/selector-nth-validation.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/selector-nth-validation.js +91 -0
- package/dist/generators/test-generator/utils/selector-nth-validation.js.map +1 -0
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +30 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +195 -15
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-type-validation.d.ts +51 -0
- package/dist/generators/test-generator/utils/selector-type-validation.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/selector-type-validation.js +77 -0
- package/dist/generators/test-generator/utils/selector-type-validation.js.map +1 -0
- package/dist/harness/data-driven-lint.d.ts.map +1 -1
- package/dist/harness/data-driven-lint.js +217 -33
- package/dist/harness/data-driven-lint.js.map +1 -1
- package/dist/harness/script-check.d.ts +2 -2
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +186 -25
- package/dist/harness/script-check.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +6 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +16 -0
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/report-sanitize-reporter-install.d.ts +24 -0
- package/dist/orchestrator/report-sanitize-reporter-install.d.ts.map +1 -0
- package/dist/orchestrator/report-sanitize-reporter-install.js +161 -0
- package/dist/orchestrator/report-sanitize-reporter-install.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +9 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +10 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +108 -8
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +9 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +4 -4
- package/dist/orchestrator/templates/playwright.config.ts +7 -0
- package/dist/orchestrator/templates/specs-checked-state.d.ts +83 -0
- package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-checked-state.js +261 -0
- package/dist/orchestrator/templates/specs-checked-state.js.map +1 -0
- package/dist/orchestrator/templates/specs-checked-state.ts +324 -0
- package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.d.ts +36 -0
- package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.js +155 -0
- package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.js.map +1 -0
- package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.ts +142 -0
- package/dist/orchestrator/templates/specs-url-assert.d.ts +50 -25
- package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.js +65 -24
- package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.ts +101 -28
- package/dist/utils/selector-types.d.ts +43 -1
- package/dist/utils/selector-types.d.ts.map +1 -1
- package/dist/utils/selector-types.js +51 -7
- package/dist/utils/selector-types.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/commands/delivery.ts +2 -1
- package/src/cli/commands/generate.ts +5 -2
- package/src/cli/commands/inspect.ts +1 -1
- package/src/cli/commands/results-merge.ts +9 -2
- package/src/cli/commands/results-sanitize.ts +98 -0
- package/src/cli/commands/script-check.ts +3 -1
- package/src/cli/commands/update.ts +3 -0
- package/src/exporters/api-testcase-formatter.ts +5 -7
- package/src/exporters/matrix/build.ts +4 -2
- package/src/exporters/matrix/render-coverage-sheet.ts +3 -1
- package/src/exporters/matrix/render-csv.ts +3 -1
- package/src/exporters/matrix/render-overview-sheet.ts +5 -10
- package/src/exporters/matrix/result-totals.ts +37 -0
- package/src/exporters/matrix/testcase-summary-band.ts +2 -7
- package/src/exporters/qa-env-secret-denylist.ts +1 -1
- package/src/exporters/report-path-sanitizer.ts +144 -0
- package/src/exporters/result-variants.ts +2 -1
- package/src/exporters/sensitive-key.ts +48 -0
- package/src/exporters/test-data-resolver.ts +12 -2
- package/src/generators/gherkin-parser/index.ts +21 -10
- package/src/generators/gherkin-parser/parent-scope-clause.ts +130 -0
- package/src/generators/test-generator/adapters/adapter-interface.ts +25 -3
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +4 -4
- package/src/generators/test-generator/adapters/appium/templates/after-all.hbs +18 -0
- package/src/generators/test-generator/adapters/appium/templates/after-each.hbs +18 -0
- package/src/generators/test-generator/adapters/appium/templates/before-all.hbs +18 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-at-position-action.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-element-with-text.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/double-click-action.hbs +7 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/frame-enter-action.hbs +3 -6
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/table-paginate-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/long-press-action.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/pinch-zoom-action.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -1
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +54 -23
- package/src/generators/test-generator/adapters/index.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +4 -4
- package/src/generators/test-generator/adapters/playwright/templates/after-all.hbs +19 -1
- package/src/generators/test-generator/adapters/playwright/templates/after-each.hbs +18 -0
- package/src/generators/test-generator/adapters/playwright/templates/before-all.hbs +19 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/check-action.hbs +6 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-at-position-action.hbs +23 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +8 -8
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +7 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-paginate-action.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/uncheck-action.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/wait-for-page.hbs +18 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +32 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +13 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +2 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +14 -3
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +13 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +3 -3
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/checked-opts.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +10 -8
- package/src/generators/test-generator/code-generator.ts +176 -45
- package/src/generators/test-generator/diagnostics.ts +79 -5
- package/src/generators/test-generator/patterns/index.ts +97 -16
- package/src/generators/test-generator/patterns/with-parent-scope.ts +41 -0
- package/src/generators/test-generator/step-mapper.ts +170 -26
- package/src/generators/test-generator/template-engine.ts +21 -21
- package/src/generators/test-generator/utils/data-resolver.ts +23 -2
- package/src/generators/test-generator/utils/dialog-scope-identity.ts +71 -0
- package/src/generators/test-generator/utils/escape-js-string.ts +24 -0
- package/src/generators/test-generator/utils/nearest-match.ts +55 -0
- package/src/generators/test-generator/utils/selector-entry-fields.ts +131 -0
- package/src/generators/test-generator/utils/selector-entry-walk.ts +58 -0
- package/src/generators/test-generator/utils/selector-nth-validation.ts +99 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +245 -16
- package/src/generators/test-generator/utils/selector-type-validation.ts +87 -0
- package/src/harness/data-driven-lint.ts +192 -32
- package/src/harness/script-check.ts +186 -21
- package/src/index.ts +7 -0
- package/src/orchestrator/project-initializer.ts +17 -0
- package/src/orchestrator/report-sanitize-reporter-install.ts +109 -0
- package/src/orchestrator/templates/ai-src/commands/run-test.md +9 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +10 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +108 -8
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +9 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +4 -4
- package/src/orchestrator/templates/playwright.config.ts +7 -0
- package/src/orchestrator/templates/specs-checked-state.ts +324 -0
- package/src/orchestrator/templates/specs-report-path-sanitize-reporter.ts +142 -0
- package/src/orchestrator/templates/specs-url-assert.ts +101 -28
- package/src/utils/selector-types.ts +54 -5
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { ParsedStep } from '../gherkin-parser';
|
|
2
|
+
import { PARENT_CONTAINER_TYPES } from '../gherkin-parser/parent-scope-clause';
|
|
2
3
|
import { TemplateEngine } from './template-engine';
|
|
3
4
|
import { adapterRegistry } from './adapters';
|
|
4
5
|
import { PatternRegistry, PatternContext, Platform } from './patterns';
|
|
5
6
|
import { SelectorResolver } from './utils/selector-resolver';
|
|
6
7
|
import { DataResolver } from './utils/data-resolver';
|
|
8
|
+
import { locatorIdentityKey } from './utils/dialog-scope-identity';
|
|
7
9
|
import { DiagnosticsCollector } from './diagnostics';
|
|
8
10
|
import { unsupportedStepCode } from './patterns/unsupported-step-code';
|
|
11
|
+
import { maskRefsForMatching } from './utils/mask-refs-for-matching';
|
|
12
|
+
import { isKnownSelectorType } from './utils/selector-type-validation';
|
|
9
13
|
import { loadAppProfile, ResolvedAppProfile } from './app-profile';
|
|
10
14
|
import { capabilityRegistry } from '../../capabilities/registry';
|
|
11
15
|
|
|
@@ -38,6 +42,8 @@ export interface MappedStep {
|
|
|
38
42
|
code: string; // Generated Playwright code
|
|
39
43
|
comment?: string; // Optional comment
|
|
40
44
|
imports?: string[]; // Required imports
|
|
45
|
+
pattern?: string; // Name of the step form that produced the code (post-map guards read it)
|
|
46
|
+
templateData?: Record<string, any>; // What the resolver handed the template (e.g. a recorded refusal)
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
/**
|
|
@@ -203,6 +209,19 @@ export class StepMapper {
|
|
|
203
209
|
this.dataResolver.registerCaptured(name);
|
|
204
210
|
}
|
|
205
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Register the head of a variable bound by the feature's `@beforeAll` hook. Feature-scoped: it
|
|
214
|
+
* survives setScenarioContext() so every scenario can read `{{head.*}}` as a runtime get().
|
|
215
|
+
*/
|
|
216
|
+
registerFeatureCaptured(head: string): void {
|
|
217
|
+
this.dataResolver.registerFeatureCaptured(head);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Drop feature-scoped captured vars — call once per feature, before its hooks and scenarios map. */
|
|
221
|
+
clearFeatureCaptured(): void {
|
|
222
|
+
this.dataResolver.clearFeatureCaptured();
|
|
223
|
+
}
|
|
224
|
+
|
|
206
225
|
/**
|
|
207
226
|
* Map a Gherkin step to Playwright code
|
|
208
227
|
* Uses pattern registry first, falls back to AI if enabled
|
|
@@ -216,6 +235,8 @@ export class StepMapper {
|
|
|
216
235
|
this.lastPrimaryKeyword = keyword;
|
|
217
236
|
}
|
|
218
237
|
|
|
238
|
+
this.warnScopeClauseDiscarded(step);
|
|
239
|
+
|
|
219
240
|
// SG-W015 — `see [X] card with {{n}}` where n is a bare number compiles to a TEXT
|
|
220
241
|
// assertion (toHaveText('3')), not a count. An author reaching for "3 cards are shown"
|
|
221
242
|
// gets an assertion that can only pass if the card's TEXT is the digit — a wrong test
|
|
@@ -295,20 +316,25 @@ export class StepMapper {
|
|
|
295
316
|
this.templateEngine.setBaseContext({ inFrame: true, frameSelector });
|
|
296
317
|
// C1 — routed through the adapter instead of building `page.frameLocator(...)` inline:
|
|
297
318
|
// the Appium adapter's own template performs its webview-context switch instead.
|
|
298
|
-
|
|
319
|
+
// `frameRef` carries the name as WRITTEN in the Gherkin: the Appium twin can fail (#705),
|
|
320
|
+
// and its error has to quote the step the author would go and look at, not the resolved
|
|
321
|
+
// xpath they never typed.
|
|
322
|
+
const code = this.templateEngine.renderStep('frame-enter-action', { frameSelector, frameRef: selectorRef });
|
|
299
323
|
return { code, comment: `Switch to ${selectorRef} frame` };
|
|
300
324
|
}
|
|
301
325
|
|
|
302
326
|
// Dialog scope directives — intercept before pattern matching
|
|
303
327
|
if (/\buse dialog\b/i.test(step.text)) {
|
|
304
328
|
this.inDialogScope = true;
|
|
305
|
-
|
|
329
|
+
// setBaseContext MERGES — an earlier scope's root keys in the same scenario would
|
|
330
|
+
// otherwise leak into this unnamed one.
|
|
331
|
+
this.templateEngine.setBaseContext({ inDialog: true, dialogScopeKey: undefined });
|
|
306
332
|
this.warnDialogScopeIgnoredOnMobile(step);
|
|
307
333
|
return { code: '', comment: 'Enter dialog scope' };
|
|
308
334
|
}
|
|
309
335
|
if (/\b(?:close|dismiss|exit)\s+dialog\b/i.test(step.text)) {
|
|
310
336
|
this.inDialogScope = false;
|
|
311
|
-
this.templateEngine.setBaseContext({ inDialog: false });
|
|
337
|
+
this.templateEngine.setBaseContext({ inDialog: false, dialogScopeKey: undefined });
|
|
312
338
|
return { code: '', comment: 'Exit dialog scope' };
|
|
313
339
|
}
|
|
314
340
|
// `is on [X] <type>` supports exactly two shapes: `page` (URL from the page-type selector
|
|
@@ -344,6 +370,7 @@ export class StepMapper {
|
|
|
344
370
|
dialogRootExpr: undefined,
|
|
345
371
|
dialogRoleName: undefined,
|
|
346
372
|
dialogFilterText: undefined,
|
|
373
|
+
dialogScopeKey: undefined,
|
|
347
374
|
};
|
|
348
375
|
|
|
349
376
|
if (step.selectorRef && step.dataRef) {
|
|
@@ -372,6 +399,16 @@ export class StepMapper {
|
|
|
372
399
|
}
|
|
373
400
|
// Case 1: no selectorRef → plain page.getByRole('dialog')
|
|
374
401
|
|
|
402
|
+
// Record what this root IS, so a later step naming the same element is recognized as the
|
|
403
|
+
// root rather than nested inside it (issue #692). The declared entry when there is one;
|
|
404
|
+
// otherwise the named-dialog fallback the element itself will resolve to.
|
|
405
|
+
if (step.selectorRef) {
|
|
406
|
+
const entry = this.resolveDialogEntry(step.selectorRef);
|
|
407
|
+
contextVars.dialogScopeKey = entry
|
|
408
|
+
? locatorIdentityKey(entry)
|
|
409
|
+
: locatorIdentityKey({ strategy: 'role', role: 'dialog', name: step.selectorRef });
|
|
410
|
+
}
|
|
411
|
+
|
|
375
412
|
this.templateEngine.setBaseContext(contextVars);
|
|
376
413
|
return { code: '', comment: `Enter dialog scope${step.selectorRef ? ': ' + step.selectorRef : ''}` };
|
|
377
414
|
}
|
|
@@ -427,10 +464,14 @@ export class StepMapper {
|
|
|
427
464
|
this.checkPageAssertionNoOpOnMobile(step, mappedStep);
|
|
428
465
|
this.checkSetupNoOpOnMobile(step, mappedStep);
|
|
429
466
|
this.checkNavigationNoOpOnMobile(step, mappedStep);
|
|
430
|
-
this.
|
|
467
|
+
this.checkPageStepFallback(step, mappedStep);
|
|
431
468
|
this.checkDeclaredUrlHasNoQuery(step, mappedStep);
|
|
469
|
+
const positionDropped = this.checkClickPositionDropped(step, mappedStep);
|
|
470
|
+
if (positionDropped) return positionDropped;
|
|
432
471
|
const swallowed = this.checkAssertionClauseSwallowed(step, mappedStep);
|
|
433
472
|
if (swallowed) return swallowed;
|
|
473
|
+
const badType = this.checkUnknownSelectorType(step, mappedStep);
|
|
474
|
+
if (badType) return badType;
|
|
434
475
|
return mappedStep;
|
|
435
476
|
}
|
|
436
477
|
|
|
@@ -487,6 +528,27 @@ export class StepMapper {
|
|
|
487
528
|
* falls back to the legacy named-dialog root, so behavior is unchanged for those entries.
|
|
488
529
|
*/
|
|
489
530
|
private renderDialogRoot(selectorRef: string, opts: { nonRoleOnly?: boolean } = {}): string | undefined {
|
|
531
|
+
const resolved = this.resolveDialogEntry(selectorRef);
|
|
532
|
+
if (!resolved) return undefined;
|
|
533
|
+
if (!StepMapper.DIALOG_ROOT_STRATEGIES.includes(resolved.strategy)) return undefined;
|
|
534
|
+
if (opts.nonRoleOnly && resolved.strategy === 'role') return undefined;
|
|
535
|
+
|
|
536
|
+
// Render the root PAGE-rooted: leaving inDialog set (or `scope: dialog` on the entry
|
|
537
|
+
// itself) would send the partial back through dialog-root and nest the dialog in itself.
|
|
538
|
+
const expr = this.templateEngine.renderStep('locator', {
|
|
539
|
+
...resolved,
|
|
540
|
+
scope: resolved.scope === 'dialog' ? undefined : resolved.scope,
|
|
541
|
+
inDialog: false,
|
|
542
|
+
}).trim();
|
|
543
|
+
return expr || undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* The selectors.yaml entry behind a dialog reference, normalized the way the `locator` partial
|
|
548
|
+
* needs it. Shared by `renderDialogRoot` (which turns it into the root expression) and the
|
|
549
|
+
* scope-entry path (which keys off it to recognize the root again later — issue #692).
|
|
550
|
+
*/
|
|
551
|
+
private resolveDialogEntry(selectorRef: string): any | undefined {
|
|
490
552
|
let resolved: any;
|
|
491
553
|
try {
|
|
492
554
|
resolved = this.selectorResolver.resolveSelector(selectorRef, this.featureName, 'dialog', 0);
|
|
@@ -500,34 +562,27 @@ export class StepMapper {
|
|
|
500
562
|
if (resolved.strategy === 'css' && resolved.locator) {
|
|
501
563
|
resolved = { ...resolved, strategy: 'locator', value: resolved.locator };
|
|
502
564
|
}
|
|
503
|
-
|
|
504
|
-
if (opts.nonRoleOnly && resolved.strategy === 'role') return undefined;
|
|
505
|
-
|
|
506
|
-
// Render the root PAGE-rooted: leaving inDialog set (or `scope: dialog` on the entry
|
|
507
|
-
// itself) would send the partial back through dialog-root and nest the dialog in itself.
|
|
508
|
-
const expr = this.templateEngine.renderStep('locator', {
|
|
509
|
-
...resolved,
|
|
510
|
-
scope: resolved.scope === 'dialog' ? undefined : resolved.scope,
|
|
511
|
-
inDialog: false,
|
|
512
|
-
}).trim();
|
|
513
|
-
return expr || undefined;
|
|
565
|
+
return resolved;
|
|
514
566
|
}
|
|
515
567
|
|
|
516
568
|
/**
|
|
517
|
-
* SG-W013 — a page
|
|
518
|
-
* selector entry.
|
|
519
|
-
*
|
|
520
|
-
* anchored that produced a test which always PASSED; anchored, it produces
|
|
521
|
-
* pass
|
|
569
|
+
* SG-W013 — a page STEP whose path came from a fallback rather than from a `type: page`
|
|
570
|
+
* selector entry. Every resolver degrades quietly when `[Ref]` does not resolve: `see [X] page`
|
|
571
|
+
* and `wait for [X] page` fall back to the feature's own path, `is on [X] page` to `/<ref>/`.
|
|
572
|
+
* Before the match was anchored that produced a test which always PASSED; anchored, it produces
|
|
573
|
+
* one that can never pass — an assertion that always fails, or, since #677 anchored the wait
|
|
574
|
+
* too, a wait that can only time out. None is the step the author wrote, and nothing said so.
|
|
522
575
|
*
|
|
523
|
-
* Detected here rather than in the resolver on purpose:
|
|
524
|
-
*
|
|
525
|
-
*
|
|
576
|
+
* Detected here rather than in the resolver on purpose: the resolvers live in the driver packages
|
|
577
|
+
* while the collector lives in core (the mapped step now also names its pattern and carries the
|
|
578
|
+
* resolver's data, but the fallback itself is only visible by re-resolving the selector). Re-asking the SAME resolver the step-mapper already owns costs three lines and
|
|
526
579
|
* duplicates no logic — the alternative is widening the adapter contract for every driver.
|
|
527
580
|
*/
|
|
528
|
-
private
|
|
581
|
+
private checkPageStepFallback(step: ParsedStep, mapped: MappedStep): void {
|
|
529
582
|
if (!this.diagnostics || !step.selectorRef || step.elementType !== 'page') return;
|
|
530
|
-
|
|
583
|
+
// every shape that compiles a page path into an ANCHORED match: the two assertions (#662)
|
|
584
|
+
// and the wait (#677). A step that emitted none of them has no path to get wrong.
|
|
585
|
+
if (!/\b(?:urlMatches|expectPage|waitForPage)\(/.test(mapped.code || '')) return;
|
|
531
586
|
let resolvedAsPage = false;
|
|
532
587
|
try {
|
|
533
588
|
const resolved = this.selectorResolver.resolveSelector(
|
|
@@ -539,7 +594,7 @@ export class StepMapper {
|
|
|
539
594
|
}
|
|
540
595
|
if (resolvedAsPage) return;
|
|
541
596
|
this.diagnostics.add('SG-W013',
|
|
542
|
-
`page
|
|
597
|
+
`page step "[${step.selectorRef}]" has no "type: page" selector entry, so it targets a FALLBACK path — the anchored match can never reach the real URL (an assertion always fails; a wait can only time out). Declare the page, e.g. "${step.selectorRef.toLowerCase()}:\\n type: page\\n value: '/your/path'"`,
|
|
543
598
|
{ feature: this.featureName, step: step.text });
|
|
544
599
|
}
|
|
545
600
|
|
|
@@ -588,6 +643,43 @@ export class StepMapper {
|
|
|
588
643
|
* Deliberately narrow — it fires only on the bare-visibility shape, so a pattern that genuinely
|
|
589
644
|
* consumed the clause (`is-hidden`, `contains`, `matches-format`, the state family) is untouched.
|
|
590
645
|
*/
|
|
646
|
+
/**
|
|
647
|
+
* SG-E027 (#711) — the step names an entry whose `type:` is not a selector type, so the resolver
|
|
648
|
+
* fell to its placeholder default and this step compiled to `getByPlaceholder('<the value>')`:
|
|
649
|
+
* a locator that cannot match, which would fail much later as a timeout naming placeholder text
|
|
650
|
+
* the author never wrote. Compile it to a `throw` instead — the same reasoning as SG-E023 above,
|
|
651
|
+
* that a step which reads as a check and can only ever fail obscurely is worse than one that
|
|
652
|
+
* fails loudly and says why.
|
|
653
|
+
*
|
|
654
|
+
* The DIAGNOSTIC is not added here. `SelectorResolver.loadNewSelectorFile` already reported this
|
|
655
|
+
* entry once for the whole file; re-reporting per step would print the same line twenty times for
|
|
656
|
+
* one typo. This method only replaces the code, so the compile-time message and the runtime
|
|
657
|
+
* failure stay one fact reported once.
|
|
658
|
+
*
|
|
659
|
+
* A resolve throw means the key is missing entirely — a different defect that the resolver's own
|
|
660
|
+
* error already owns — and an auto-inferred selector stamps no `selectorType`, so both skip.
|
|
661
|
+
*/
|
|
662
|
+
private checkUnknownSelectorType(step: ParsedStep, mapped: MappedStep): MappedStep | null {
|
|
663
|
+
if (!step.selectorRef) return null;
|
|
664
|
+
let selectorType: unknown;
|
|
665
|
+
try {
|
|
666
|
+
selectorType = this.selectorResolver.resolveSelector(
|
|
667
|
+
step.selectorRef, this.featureName, step.elementType, step.nth,
|
|
668
|
+
).selectorType;
|
|
669
|
+
} catch {
|
|
670
|
+
return null; // missing key — the resolver's own error owns that
|
|
671
|
+
}
|
|
672
|
+
if (selectorType === undefined || isKnownSelectorType(selectorType)) return null;
|
|
673
|
+
const reason = `step "${step.text}" names selector "${step.selectorRef}", whose selectors.yaml `
|
|
674
|
+
+ `entry declares type: "${String(selectorType)}" — not a selector type, so the entry `
|
|
675
|
+
+ `resolved to a placeholder locator that cannot match. See SG-E027 for the valid types and `
|
|
676
|
+
+ `the nearest spelling to what was written.`;
|
|
677
|
+
return {
|
|
678
|
+
code: unsupportedStepCode(reason),
|
|
679
|
+
comment: `Unknown selector type "${String(selectorType)}" — see SG-E027: ${step.text}`,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
591
683
|
private checkAssertionClauseSwallowed(step: ParsedStep, mapped: MappedStep): MappedStep | null {
|
|
592
684
|
const code = mapped.code || '';
|
|
593
685
|
// Bare visibility: nothing but a visible/displayed assertion on one locator.
|
|
@@ -630,6 +722,42 @@ export class StepMapper {
|
|
|
630
722
|
};
|
|
631
723
|
}
|
|
632
724
|
|
|
725
|
+
/**
|
|
726
|
+
* A click step that names a POSITION (`at position "top-left"`, `at offset x=…, y=…`) must compile
|
|
727
|
+
* through the positioned-click form with a position it understood. Anything else — a typo in the
|
|
728
|
+
* keyword, a value the grammar rejects, or a combination another click form claims first (`try to
|
|
729
|
+
* click … at position`, `… at offset … and accept [OK] alert`, `… with {{v}} … at position`) —
|
|
730
|
+
* would otherwise click the element's CENTRE and pass, which for a backdrop-dismiss test is the
|
|
731
|
+
* one outcome that proves nothing. Owned by the compiler rather than the driver so it holds no
|
|
732
|
+
* matter which pattern wins.
|
|
733
|
+
*/
|
|
734
|
+
private static readonly CLICK_POSITION_HINT =
|
|
735
|
+
/\bat\s+(?:position|offset)\b|\bat\s+\w+\s+(?:"(?:top|bottom)-(?:left|right)"|x\s*=)|\b(?:top|bottom)[- ](?:left|right)\b|\bat\s+x\s*=/i;
|
|
736
|
+
|
|
737
|
+
private checkClickPositionDropped(step: ParsedStep, mapped: MappedStep): MappedStep | null {
|
|
738
|
+
// Refs are masked; so is every quoted VALUE except a preset name — `with "Engineer at position 3"`
|
|
739
|
+
// is a text filter, not a position, while a mistyped `at postion "top-left"` must stay visible.
|
|
740
|
+
const masked = maskRefsForMatching(step.text)
|
|
741
|
+
.replace(/"([^"]*)"/g, (quoted, inner: string) => (/^(?:top|bottom)-(?:left|right)$/i.test(inner.trim()) ? quoted : '"~"'));
|
|
742
|
+
if (!/\bclick/i.test(masked) || !StepMapper.CLICK_POSITION_HINT.test(masked)) return null;
|
|
743
|
+
// Already refused upstream (off-platform SG-W020, a dropped state clause, a missing template…):
|
|
744
|
+
// that refusal names the real cause; this guard only exists to stop a SILENT centre click.
|
|
745
|
+
if (/^throw new Error\("\[sungen\]/.test(mapped.code.trim())) return null;
|
|
746
|
+
const refused = mapped.templateData?.positionError as string | undefined;
|
|
747
|
+
if (mapped.pattern === 'click-element-at-position' && !refused) return null;
|
|
748
|
+
const reason = refused
|
|
749
|
+
? `step "${step.text}": ${refused}`
|
|
750
|
+
: `step "${step.text}" asks for a click position but compiled through the "${mapped.pattern ?? 'unknown'}" `
|
|
751
|
+
+ 'step form, which clicks the element\'s centre — the position would be silently dropped. Write '
|
|
752
|
+
+ 'it as `click [X] <type> at position "<preset>"` or `… at offset x=<n>px|%, y=<n>px|%` on its own: '
|
|
753
|
+
+ '`try to click`, a `with {{v}}` text filter and `… and accept [OK] alert` do not take a position.';
|
|
754
|
+
this.diagnostics?.add('SG-E026', reason, { feature: this.featureName, step: step.text });
|
|
755
|
+
return {
|
|
756
|
+
code: unsupportedStepCode(reason),
|
|
757
|
+
comment: `Click position dropped — see SG-E026: ${step.text}`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
633
761
|
private checkDataRefConsumed(step: ParsedStep, mapped: MappedStep): void {
|
|
634
762
|
if (!step.dataRef || !this.diagnostics) return;
|
|
635
763
|
const code = mapped.code || '';
|
|
@@ -839,6 +967,22 @@ export class StepMapper {
|
|
|
839
967
|
{ feature: this.featureName, step: step.text });
|
|
840
968
|
}
|
|
841
969
|
|
|
970
|
+
/**
|
|
971
|
+
* SG-W028 — a trailing `in [X] <word>` scope clause whose `<word>` is not a container type.
|
|
972
|
+
*
|
|
973
|
+
* The clause is dropped, and everything downstream takes the FIRST `[ref]` and the FIRST
|
|
974
|
+
* `{{data}}` it finds, so the sentence compiles clean while the assertion silently widens to the
|
|
975
|
+
* whole screen (#667). Unlike an unrecognized STEP, which at least raises SG-W001, a partially
|
|
976
|
+
* understood step used to say nothing at all — and a check that runs against more than it names
|
|
977
|
+
* passes for the wrong reason in exactly the list-of-identical-cards case it was written for.
|
|
978
|
+
*/
|
|
979
|
+
private warnScopeClauseDiscarded(step: ParsedStep): void {
|
|
980
|
+
if (!step.unsupportedScopeType) return;
|
|
981
|
+
this.diagnostics?.add('SG-W028',
|
|
982
|
+
`\`${step.unsupportedScopeType}\` is not a container type, so the scope clause was DROPPED and this step runs against the whole page, not the named container. Supported: ${PARENT_CONTAINER_TYPES.join(', ')}`,
|
|
983
|
+
{ feature: this.featureName, step: step.text });
|
|
984
|
+
}
|
|
985
|
+
|
|
842
986
|
/**
|
|
843
987
|
* SG-W021 — dialog scope is a no-op on the Appium adapter.
|
|
844
988
|
*
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import Handlebars from 'handlebars';
|
|
2
|
+
import { escapeSingleQuoted } from './utils/escape-js-string';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import path from 'path';
|
|
4
|
-
import type { RenderImportsOptions } from './adapters/adapter-interface';
|
|
5
|
+
import type { RenderImportsOptions, HookRenderData } from './adapters/adapter-interface';
|
|
6
|
+
import { locatorIdentityKey } from './utils/dialog-scope-identity';
|
|
5
7
|
|
|
6
8
|
export class TemplateEngine {
|
|
7
9
|
private templates: Map<string, HandlebarsTemplateDelegate> = new Map();
|
|
@@ -28,16 +30,9 @@ export class TemplateEngine {
|
|
|
28
30
|
// the result in '…' (selector values, attribute values, scope), so a value containing a newline,
|
|
29
31
|
// backslash, or quote must be escaped or the generated spec is a syntax error (e.g. a Flutter
|
|
30
32
|
// composite content-desc like "Quick Convert\nSelect File\n…" used as an accessibility-id).
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return String(text)
|
|
35
|
-
.replace(/\\/g, '\\\\')
|
|
36
|
-
.replace(/'/g, "\\'")
|
|
37
|
-
.replace(/\n/g, '\\n')
|
|
38
|
-
.replace(/\r/g, '\\r')
|
|
39
|
-
.replace(/\t/g, '\\t');
|
|
40
|
-
});
|
|
33
|
+
// The rule itself lives in utils/escape-js-string so the pattern layer, which builds some
|
|
34
|
+
// locator strings by hand, cannot drift from what the templates do.
|
|
35
|
+
Handlebars.registerHelper('escapeQuotes', (text: string) => escapeSingleQuoted(text));
|
|
41
36
|
|
|
42
37
|
// Escape a raw string for embedding inside a REGEX LITERAL (/…/). Metacharacters first —
|
|
43
38
|
// that class doubles a backslash, so the control-char passes below cannot be mis-read as
|
|
@@ -152,6 +147,17 @@ export class TemplateEngine {
|
|
|
152
147
|
// element is dialog-rooted even if its role is portal-exempt (issue #465).
|
|
153
148
|
const explicitDialog = this.scope === 'dialog';
|
|
154
149
|
if (!this.inDialog && !explicitDialog) return 'page';
|
|
150
|
+
// The element being resolved IS the active scope's own dialog root (issue #692). Prefixing
|
|
151
|
+
// it with that root asks for a dialog nested inside itself —
|
|
152
|
+
// getByRole('dialog',{name:X}).getByRole('dialog',{name:X}) — which can never resolve, yet
|
|
153
|
+
// compiles clean and fails as a plain timeout reading "the dialog never opened". So the
|
|
154
|
+
// root is idempotent: re-asserting the dialog you are scoped inside means what it reads as.
|
|
155
|
+
// Skipped for a `with {{v}}` root, whose .filter({ hasText }) form is not expressible as
|
|
156
|
+
// the element's own locator — identity says nothing about it there.
|
|
157
|
+
if (this.inDialog && !this.dialogFilterText && this.dialogScopeKey &&
|
|
158
|
+
locatorIdentityKey(this) === this.dialogScopeKey) {
|
|
159
|
+
return 'page';
|
|
160
|
+
}
|
|
155
161
|
// Portal-rendered roles (Radix/MUI/Headless UI/antd render option/menu/tooltip
|
|
156
162
|
// layers onto document.body) live OUTSIDE the dialog's DOM subtree — wrapping
|
|
157
163
|
// them in the dialog root guarantees a timeout. List comes from the app profile
|
|
@@ -297,7 +303,7 @@ export class TemplateEngine {
|
|
|
297
303
|
}
|
|
298
304
|
|
|
299
305
|
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 });
|
|
306
|
+
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, checkedStateImports: options?.checkedStateImports });
|
|
301
307
|
}
|
|
302
308
|
|
|
303
309
|
renderTestFile(data: {
|
|
@@ -332,21 +338,15 @@ export class TemplateEngine {
|
|
|
332
338
|
return this.render('before-each', data);
|
|
333
339
|
}
|
|
334
340
|
|
|
335
|
-
renderBeforeAll(data: {
|
|
336
|
-
steps: Array<{ comment?: string; code: string }>;
|
|
337
|
-
}): string {
|
|
341
|
+
renderBeforeAll(data: HookRenderData): string {
|
|
338
342
|
return this.render('before-all', data);
|
|
339
343
|
}
|
|
340
344
|
|
|
341
|
-
renderAfterEach(data: {
|
|
342
|
-
steps: Array<{ comment?: string; code: string }>;
|
|
343
|
-
}): string {
|
|
345
|
+
renderAfterEach(data: HookRenderData): string {
|
|
344
346
|
return this.render('after-each', data);
|
|
345
347
|
}
|
|
346
348
|
|
|
347
|
-
renderAfterAll(data: {
|
|
348
|
-
steps: Array<{ comment?: string; code: string }>;
|
|
349
|
-
}): string {
|
|
349
|
+
renderAfterAll(data: HookRenderData): string {
|
|
350
350
|
return this.render('after-all', data);
|
|
351
351
|
}
|
|
352
352
|
|
|
@@ -29,6 +29,27 @@ export class DataResolver {
|
|
|
29
29
|
this.capturedVars.clear();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Vars bound by a `@beforeAll` hook's `@api:`/`@query:` preconditions. The hook runs before every
|
|
33
|
+
// test of the feature, so its bindings are readable from EVERY scenario — they survive the
|
|
34
|
+
// per-scenario clearCaptured(). Stored by head (`login`) and matched against the ref's head
|
|
35
|
+
// (`login.body.token` → `login`), since a scenario may read any sub-path of the bound response.
|
|
36
|
+
private featureCaptured = new Set<string>();
|
|
37
|
+
|
|
38
|
+
/** Register the head of a variable bound by the feature's `@beforeAll` hook (feature-scoped). */
|
|
39
|
+
registerFeatureCaptured(head: string): void {
|
|
40
|
+
this.featureCaptured.add(head);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Clear feature-scoped captured vars — call at the start of each feature. */
|
|
44
|
+
clearFeatureCaptured(): void {
|
|
45
|
+
this.featureCaptured.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Runtime-only ref (scenario-captured, or bound by the feature's `@beforeAll` hook)? */
|
|
49
|
+
private isCaptured(dataRef: string): boolean {
|
|
50
|
+
return this.capturedVars.has(dataRef) || this.featureCaptured.has(dataRef.split(/[.[]/)[0]);
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
constructor(testDataDir?: string, screenName?: string, runtimeMode: boolean = false) {
|
|
33
54
|
this.testDataDir = testDataDir || path.join(process.cwd(), 'qa', 'test-data');
|
|
34
55
|
this.screenName = screenName;
|
|
@@ -52,7 +73,7 @@ export class DataResolver {
|
|
|
52
73
|
resolveData(dataRef: string, featureName?: string): string {
|
|
53
74
|
// Captured vars exist only at runtime → emit a marker (→ testData.get) and
|
|
54
75
|
// skip YAML validation. Requires runtime mode (capture writes testData.set).
|
|
55
|
-
if (this.
|
|
76
|
+
if (this.isCaptured(dataRef)) {
|
|
56
77
|
return DataResolver.encodeMarker(dataRef);
|
|
57
78
|
}
|
|
58
79
|
|
|
@@ -75,7 +96,7 @@ export class DataResolver {
|
|
|
75
96
|
* anything said about it belongs in a warning, never a gate.
|
|
76
97
|
*/
|
|
77
98
|
peekBaseValue(dataRef: string, featureName?: string): string | undefined {
|
|
78
|
-
if (this.
|
|
99
|
+
if (this.isCaptured(dataRef)) return undefined;
|
|
79
100
|
try {
|
|
80
101
|
return this.resolveValue(dataRef, featureName);
|
|
81
102
|
} catch {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of a locator as the `locator` partial actually renders it — issue #692.
|
|
3
|
+
*
|
|
4
|
+
* `Given User is on [X] dialog` records a scope root, and the `pageRoot` helper prefixes every
|
|
5
|
+
* locator rendered afterwards with it. That is the whole point of the scope — except when a later
|
|
6
|
+
* step names the SAME element as the scope root, where the prefix nests the dialog inside itself:
|
|
7
|
+
*
|
|
8
|
+
* page.getByRole('dialog', { name: 'Confirm' }).getByRole('dialog', { name: 'Confirm' })
|
|
9
|
+
*
|
|
10
|
+
* A dialog cannot contain itself, so that query can never resolve. It compiles clean, emits no
|
|
11
|
+
* diagnostic, and fails as a plain timeout — which reads as "the dialog never opened" rather than
|
|
12
|
+
* "the query is impossible". Re-asserting the dialog you are scoped inside is the natural thing to
|
|
13
|
+
* write straight after the `Given`, so the scope root is made idempotent instead.
|
|
14
|
+
*
|
|
15
|
+
* The guard needs exactly one question answered: does this element render to the same locator as
|
|
16
|
+
* the active scope root? So the key carries the fields the partial reads FOR THAT STRATEGY and
|
|
17
|
+
* nothing else — two entries share a key iff they render the same expression. Comparing role+name
|
|
18
|
+
* alone (the shape the issue first suggested) would miss `type: locator` and `type: testid`
|
|
19
|
+
* dialogs, which self-nest just the same.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately says nothing about a `with {{v}}` filtered root: that root is
|
|
22
|
+
* `getByRole('dialog').filter({ hasText })`, which is not expressible as the element's own
|
|
23
|
+
* locator, so an identity match there would be meaningless. The caller skips the guard in that
|
|
24
|
+
* case — see the `dialogFilterText` check in `pageRoot`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** The subset of a resolved selector that the `locator` partial reads. */
|
|
28
|
+
export interface LocatorIdentityFields {
|
|
29
|
+
strategy?: string;
|
|
30
|
+
value?: string;
|
|
31
|
+
role?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
exact?: boolean;
|
|
34
|
+
nth?: number;
|
|
35
|
+
scope?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A stable key for the locator `fields` renders to, or undefined when there is no strategy to
|
|
40
|
+
* render (nothing to compare, so no guard).
|
|
41
|
+
*/
|
|
42
|
+
export function locatorIdentityKey(fields: LocatorIdentityFields | undefined): string | undefined {
|
|
43
|
+
if (!fields || !fields.strategy) return undefined;
|
|
44
|
+
|
|
45
|
+
// `scope: dialog` is the dialog-rooting sentinel rather than part of the element's own
|
|
46
|
+
// identity — renderDialogRoot strips it before rendering the root for the same reason.
|
|
47
|
+
const scope = fields.scope && fields.scope !== 'dialog' ? fields.scope : '';
|
|
48
|
+
const nth = typeof fields.nth === 'number' && fields.nth >= 0 ? String(fields.nth) : '';
|
|
49
|
+
const exact = fields.exact ? '1' : '';
|
|
50
|
+
|
|
51
|
+
const parts: string[] = [];
|
|
52
|
+
switch (fields.strategy) {
|
|
53
|
+
case 'role':
|
|
54
|
+
// getByRole reads role/name/exact — never `value`.
|
|
55
|
+
parts.push('role', fields.role || '', fields.name || '', exact);
|
|
56
|
+
break;
|
|
57
|
+
case 'testid':
|
|
58
|
+
parts.push('testid', fields.value || '');
|
|
59
|
+
break;
|
|
60
|
+
case 'placeholder':
|
|
61
|
+
case 'label':
|
|
62
|
+
case 'text':
|
|
63
|
+
parts.push(fields.strategy, fields.value || '', exact);
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
// 'locator', 'id', 'css' and the partial's own default all render locator('<value>').
|
|
67
|
+
parts.push('locator', fields.value || '');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [...parts, `nth=${nth}`, `scope=${scope}`].join('|');
|
|
71
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape a raw string for embedding inside a SINGLE-QUOTED JS string literal.
|
|
3
|
+
*
|
|
4
|
+
* Order matters: backslash first, or the escapes added afterwards get re-escaped. Getting this
|
|
5
|
+
* half-right is worse than not doing it — a value ending in `\` turns `'…\'` into an escaped
|
|
6
|
+
* closing quote, and the string literal runs on into the rest of the file, so the generated
|
|
7
|
+
* `.spec.ts` stops parsing and EVERY scenario in it is lost rather than the one step.
|
|
8
|
+
*
|
|
9
|
+
* The line terminators are not cosmetic either: a card's accessible text is routinely a composite
|
|
10
|
+
* of several lines (a Flutter content-desc, a multi-line heading), and a literal newline inside
|
|
11
|
+
* `'…'` is a syntax error.
|
|
12
|
+
*
|
|
13
|
+
* Lives here rather than inside the Handlebars helper because the pattern layer also builds locator
|
|
14
|
+
* strings by hand — the parent-scope chain among them — and two copies of this rule drift.
|
|
15
|
+
*/
|
|
16
|
+
export function escapeSingleQuoted(text: unknown): string {
|
|
17
|
+
if (text == null) return '';
|
|
18
|
+
return String(text)
|
|
19
|
+
.replace(/\\/g, '\\\\')
|
|
20
|
+
.replace(/'/g, "\\'")
|
|
21
|
+
.replace(/\n/g, '\\n')
|
|
22
|
+
.replace(/\r/g, '\\r')
|
|
23
|
+
.replace(/\t/g, '\\t');
|
|
24
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nearest-known-spelling suggestion, shared by the selector-surface checks.
|
|
3
|
+
*
|
|
4
|
+
* Two checks need the same judgement about the same class of author mistake — SG-W004 on an entry
|
|
5
|
+
* FIELD nobody reads (#712) and SG-E027 on an entry `type:` VALUE nothing dispatches (#711) — so
|
|
6
|
+
* the rule lives in one place rather than being re-tuned independently in each. The tuning is the
|
|
7
|
+
* substance here, not the edit distance:
|
|
8
|
+
*
|
|
9
|
+
* - A case-only difference is returned first and always. `testId` for `testid` and
|
|
10
|
+
* `contentEditable` for `contenteditable` are the single likeliest way to write either
|
|
11
|
+
* wrongly, and both surfaces are lowercase throughout, so this is a near-certainty rather
|
|
12
|
+
* than a guess.
|
|
13
|
+
* - Otherwise a distance of 1-2 is a typo worth naming.
|
|
14
|
+
* - Anything further is a DIFFERENT word, and guessing at it sends the author somewhere wrong.
|
|
15
|
+
* `selector` is not a misspelling of `locator` — it is someone reasonably assuming a field
|
|
16
|
+
* that does not exist, and "did you mean locator?" would be a worse answer than silence.
|
|
17
|
+
* The caller supplies its own hint for that case.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Levenshtein distance. Bounded use only — these are short identifiers. */
|
|
21
|
+
function distance(a: string, b: string): number {
|
|
22
|
+
const m = a.length, n = b.length;
|
|
23
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
24
|
+
for (let i = 1; i <= m; i++) {
|
|
25
|
+
const curr = [i];
|
|
26
|
+
for (let j = 1; j <= n; j++) {
|
|
27
|
+
curr[j] = Math.min(
|
|
28
|
+
prev[j] + 1,
|
|
29
|
+
curr[j - 1] + 1,
|
|
30
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
prev = curr;
|
|
34
|
+
}
|
|
35
|
+
return prev[n];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The nearest candidate to `value`, when one is close enough to be a likely typo rather than a
|
|
40
|
+
* different word. Returns undefined when nothing is close enough to name honestly.
|
|
41
|
+
*/
|
|
42
|
+
export function nearestMatch(
|
|
43
|
+
value: string, candidates: Iterable<string>, maxDistance = 2,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
const lower = value.toLowerCase();
|
|
46
|
+
const all = [...candidates];
|
|
47
|
+
for (const c of all) if (c.toLowerCase() === lower) return c;
|
|
48
|
+
let best: string | undefined;
|
|
49
|
+
let bestDist = Infinity;
|
|
50
|
+
for (const c of all) {
|
|
51
|
+
const d = distance(lower, c.toLowerCase());
|
|
52
|
+
if (d < bestDist) { bestDist = d; best = c; }
|
|
53
|
+
}
|
|
54
|
+
return bestDist <= maxDistance ? best : undefined;
|
|
55
|
+
}
|