@sun-asterisk/sungen 3.2.29 → 3.2.30
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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install the report-path-sanitize reporter into a project — the file under `specs/generated/` and
|
|
3
|
+
* its entry in `playwright.config.ts`.
|
|
4
|
+
*
|
|
5
|
+
* A NEW project gets both from the templates at `sungen init`. An EXISTING project has a
|
|
6
|
+
* `playwright.config.ts` that sungen wrote once and never touches again, so the reporter would never
|
|
7
|
+
* reach it: `sungen update` calls this to add the reporter file (the same refresh `sungen generate`
|
|
8
|
+
* applies to `specs/generated/base.ts`) and to insert the reporter line after the JSON reporter,
|
|
9
|
+
* once. A config that has been customised beyond recognition (no `['json'` reporter line) is left
|
|
10
|
+
* alone with a printed instruction — a wrong edit to a user's config is worse than a missing reporter,
|
|
11
|
+
* and `sungen results sanitize` (run by /sungen:run-test) covers the gap.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
|
|
16
|
+
export const REPORTER_FILE = path.join('specs', 'generated', 'report-path-sanitize-reporter.ts');
|
|
17
|
+
export const REPORTER_CONFIG_ENTRY = "['./specs/generated/report-path-sanitize-reporter.ts']";
|
|
18
|
+
const TEMPLATE = 'specs-report-path-sanitize-reporter.ts';
|
|
19
|
+
|
|
20
|
+
export type ConfigPatchOutcome = 'patched' | 'present' | 'absent' | 'manual';
|
|
21
|
+
|
|
22
|
+
/** Write (or refresh) the reporter file from the bundled template. Returns true when the file changed. */
|
|
23
|
+
export function ensureReporterFile(cwd: string, dryRun: boolean = false): boolean {
|
|
24
|
+
const templatePath = path.join(__dirname, 'templates', TEMPLATE);
|
|
25
|
+
if (!fs.existsSync(templatePath)) return false;
|
|
26
|
+
const next = fs.readFileSync(templatePath, 'utf8');
|
|
27
|
+
const target = path.join(cwd, REPORTER_FILE);
|
|
28
|
+
if (fs.existsSync(target) && fs.readFileSync(target, 'utf8') === next) return false;
|
|
29
|
+
if (!dryRun) {
|
|
30
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
31
|
+
fs.writeFileSync(target, next);
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add the reporter entry to `playwright.config.ts` right after the JSON reporter line.
|
|
38
|
+
* 'patched' — inserted now
|
|
39
|
+
* 'present' — already there (idempotent)
|
|
40
|
+
* 'absent' — no playwright.config.ts (a mobile-only project, or not a sungen project)
|
|
41
|
+
* 'manual' — the config has no recognisable `['json', …]` reporter line; nothing written
|
|
42
|
+
*/
|
|
43
|
+
export function patchPlaywrightConfig(cwd: string, dryRun: boolean = false): ConfigPatchOutcome {
|
|
44
|
+
const configPath = path.join(cwd, 'playwright.config.ts');
|
|
45
|
+
if (!fs.existsSync(configPath)) return 'absent';
|
|
46
|
+
const src = fs.readFileSync(configPath, 'utf8');
|
|
47
|
+
if (src.includes('report-path-sanitize-reporter')) return 'present';
|
|
48
|
+
const patched = insertAfterJsonReporter(src);
|
|
49
|
+
if (patched === null) return 'manual';
|
|
50
|
+
if (!dryRun) fs.writeFileSync(configPath, patched);
|
|
51
|
+
return 'patched';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `sungen update` step: install file + config entry, report what happened. */
|
|
55
|
+
export function installReportSanitizeReporter(cwd: string, dryRun: boolean = false): void {
|
|
56
|
+
if (!fs.existsSync(path.join(cwd, 'playwright.config.ts'))) return; // nothing to run Playwright with — nothing to sanitize
|
|
57
|
+
// The file FIRST: a config that names a reporter module Playwright cannot find refuses to load at all.
|
|
58
|
+
const wroteFile = ensureReporterFile(cwd, dryRun);
|
|
59
|
+
if (wroteFile) console.log(`${dryRun ? '📋 would write' : '✓ wrote'} ${REPORTER_FILE}`);
|
|
60
|
+
if (!dryRun && !fs.existsSync(path.join(cwd, REPORTER_FILE))) return; // bundled template missing — leave the config alone
|
|
61
|
+
const outcome = patchPlaywrightConfig(cwd, dryRun);
|
|
62
|
+
if (outcome === 'patched') console.log(`${dryRun ? '📋 would add' : '✓ added'} the report-path-sanitize reporter to playwright.config.ts`);
|
|
63
|
+
if (outcome === 'manual') {
|
|
64
|
+
console.log(`⚠️ playwright.config.ts has no recognisable ['json', …] reporter line — add ${REPORTER_CONFIG_ENTRY} to its reporter array by hand (until then, /sungen:run-test's \`sungen results sanitize\` step keeps result files free of machine paths).`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Insert the reporter entry right after the `['json', …]` entry, whichever shape the config uses:
|
|
70
|
+
* - one entry per line (the current template): a new line with the same indentation
|
|
71
|
+
* - the whole array on one line (older templates: `reporter: [['list'], ['json', {…}]],`): inline
|
|
72
|
+
* The entry's end is found by bracket depth, so an `outputFile` expression may carry any braces or
|
|
73
|
+
* quotes. `null` when there is no `['json'` entry at all.
|
|
74
|
+
*/
|
|
75
|
+
export function insertAfterJsonReporter(src: string): string | null {
|
|
76
|
+
const eol = src.includes('\r\n') ? '\r\n' : '\n';
|
|
77
|
+
// the first `['json'` that is code, not a commented-out line
|
|
78
|
+
let start = -1;
|
|
79
|
+
for (const m of src.matchAll(/\[\s*['"]json['"]/g)) {
|
|
80
|
+
const lineStart = src.lastIndexOf('\n', m.index!) + 1;
|
|
81
|
+
if (/^\s*(\/\/|\*)/.test(src.slice(lineStart, m.index!))) continue;
|
|
82
|
+
start = m.index!; break;
|
|
83
|
+
}
|
|
84
|
+
if (start < 0) return null;
|
|
85
|
+
let depth = 0;
|
|
86
|
+
let end = -1;
|
|
87
|
+
for (let i = start; i < src.length; i++) {
|
|
88
|
+
const ch = src[i];
|
|
89
|
+
if (ch === '[' || ch === '{' || ch === '(') depth++;
|
|
90
|
+
else if (ch === ']' || ch === '}' || ch === ')') {
|
|
91
|
+
depth--;
|
|
92
|
+
if (depth === 0) { end = i; break; }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (end < 0) return null;
|
|
96
|
+
const after = end + 1;
|
|
97
|
+
const restOfLine = src.slice(after, src.indexOf('\n', after) < 0 ? src.length : src.indexOf('\n', after));
|
|
98
|
+
if (/^\s*,?\s*$/.test(restOfLine)) {
|
|
99
|
+
// the entry ends its line → add a line below it, same indentation; exactly one comma after the
|
|
100
|
+
// json entry whether or not the source had one; the file's own line ending
|
|
101
|
+
const lineStart = src.lastIndexOf('\n', start) + 1;
|
|
102
|
+
const indent = /^[ \t]*/.exec(src.slice(lineStart))![0];
|
|
103
|
+
const lineEnd = src.indexOf('\n', after);
|
|
104
|
+
const rest = lineEnd < 0 ? '' : src.slice(lineEnd + 1);
|
|
105
|
+
return `${src.slice(0, after)},${eol}${indent}${REPORTER_CONFIG_ENTRY},${lineEnd < 0 ? '' : eol}${rest}`;
|
|
106
|
+
}
|
|
107
|
+
// inline array: `…, ['json', {…}]], use: …` → `…, ['json', {…}], [reporter]], use: …`
|
|
108
|
+
return `${src.slice(0, after)}, ${REPORTER_CONFIG_ENTRY}${src.slice(after)}`;
|
|
109
|
+
}
|
|
@@ -68,7 +68,7 @@ When `platform: mobile`:
|
|
|
68
68
|
SUNGEN_SPECS='./specs/generated/<name>/<name>.spec.ts' MOBILE_PLATFORM=both npm run test:mobile
|
|
69
69
|
```
|
|
70
70
|
`wdio.conf.ts` auto-selects `@wdio/globals` specs and auto-starts Appium. iOS needs a booted Simulator and `IOS_BUNDLE_ID`/`IOS_UDID` env vars.
|
|
71
|
-
- **Results**: the wdio reporter writes `<name>-test-result.<android|ios>[.<env>].json` — one file per capability, so a both-OS run keeps both outcomes.
|
|
71
|
+
- **Results**: the wdio reporter writes `<name>-test-result.<android|ios>[.<env>].json` — one file per capability, so a both-OS run keeps both outcomes. Then run `npx sungen results sanitize` — wdio error stacks quote absolute machine paths, and the result file is committed next to the spec.
|
|
72
72
|
- **Skip** the "Playwright command guidelines" section below — it applies to web only.
|
|
73
73
|
- The phased waves (smoke → priority → full → regression), attempt budgets, and next-steps mirror the web flow — follow the `sungen-selector-fix-mobile` skill for the mobile equivalents.
|
|
74
74
|
|
|
@@ -305,6 +305,14 @@ PLAYWRIGHT_JSON_OUTPUT_NAME=specs/generated/flows/<name>/<name>-test-result.json
|
|
|
305
305
|
|
|
306
306
|
**DO NOT** pass `--reporter=...` flag — it overrides the reporters from `playwright.config.ts` and disables the JSON reporter that `sungen delivery` depends on.
|
|
307
307
|
|
|
308
|
+
**Sanitize the result file paths right after the run** — the JSON report records the operator's absolute machine paths (`/Users/<name>/…`, `C:\Users\…`) in `config`, error locations, attachments and stack traces, and the file is committed next to the spec:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npx sungen results sanitize # every *-test-result*.json under specs/generated + .sungen/reports/*.json → project-relative paths
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Projects created with a recent sungen (or refreshed by `sungen update`) run the same rewrite automatically through the `report-path-sanitize-reporter` in `playwright.config.ts`; this command is the safety net for every other project and is idempotent — an already-clean file is left untouched.
|
|
315
|
+
|
|
308
316
|
**Re-running a SUBSET (fix-and-retry, or a partially edited spec) when the per-target result file already exists:** the JSON reporter would OVERWRITE it and lose every result you didn't re-run. Write the subset's report to a temp file and MERGE it instead — same title wins, everything else is kept:
|
|
309
317
|
|
|
310
318
|
```bash
|
|
@@ -67,6 +67,16 @@ Never emit a status-only assertion for a success case. Three tiers — see also
|
|
|
67
67
|
|
|
68
68
|
Anti-pattern to avoid: re-asserting the value you just sent (`expect {{x.body.username}} is {{username}}` on the thing you created proves little) — assert a **server-derived** field (generated id, timestamp, computed status) or read it back independently.
|
|
69
69
|
|
|
70
|
+
## Alias entries (binding one endpoint twice)
|
|
71
|
+
|
|
72
|
+
To bind two responses of the SAME endpoint in one scenario (a control call next to the call under
|
|
73
|
+
test), clone the catalog entry under a second name (`create_order_control`) — identical to the
|
|
74
|
+
original except `name`/`description`. The audit gate counts identical entries as ONE endpoint: its
|
|
75
|
+
must-cover viewpoints (`contract`, `error`) are satisfied when ANY alias asserts them, so the clone
|
|
76
|
+
needs no error scenario of its own. An entry on the same route that differs in any request or
|
|
77
|
+
response detail (`encoding`, `body`, `headers`, `expect`, `fields`, schema) is a distinct endpoint
|
|
78
|
+
and keeps its own viewpoints. An omitted `datasource` resolves to the project default before comparing.
|
|
79
|
+
|
|
70
80
|
## Output discipline
|
|
71
81
|
- Enumerate the **full Tier-1 list per endpoint first**, then write incrementally (≈10–15 scenarios per Write call) — never the whole suite in one response (output-token cap).
|
|
72
82
|
- Name scenarios `VP-API-NNN <what it proves>`; keep the endpoint + technique legible.
|
|
@@ -82,6 +82,7 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
|
|
|
82
82
|
| Element is not an input | Wrong element type targeted | Change `type` or `value` to match actual element |
|
|
83
83
|
| not a select | Custom dropdown, not native `<select>` | Set the widget variant: `variant: radix-select`/`antd-select` (built-in recipes), a custom recipe name from `qa/app.yaml`, or project-wide `widgets.select` in `qa/app.yaml` |
|
|
84
84
|
| Frame not found | iframe selector wrong or doesn't exist | Fix `frame` value, verify iframe in snapshot |
|
|
85
|
+
| Timeout / not found on a step AFTER a dialog closed, and the element is plainly there in the snapshot | **Stale dialog scope** — the scenario entered dialog scope (`Given User is on [X] dialog` or `User use dialog`) and never left. Scope is sticky: it survives the click that closed the dialog, and `wait for [X] dialog is hidden` does NOT end it. Every following locator is still prefixed with the dead dialog root | **Not a selectors.yaml fix — the locator is correct.** The `.feature` needs a `close`/`dismiss`/`exit dialog` step after the dialog closes, and the `.feature` is FROZEN during run-test → **report it as a Gherkin gap for QA** and let the test fail |
|
|
85
86
|
|
|
86
87
|
### Compile diagnostics (`sungen generate` output) → fix BEFORE running
|
|
87
88
|
|
|
@@ -90,6 +91,7 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
|
|
|
90
91
|
| `SG-W001` | Step matched no pattern — sungen has no step form for the sentence. Compiles to `throw new Error("[sungen] …")` naming the step (it emitted a no-op `// TODO` before the unsupported-step gate, which let the scenario PASS asserting nothing) | Rephrase to a documented pattern (`sungen-gherkin-syntax`) or tag `@manual` with a reason. There is no silent fallback — the test goes red until one of those two is done |
|
|
91
92
|
| `SG-W002` | A step's `{{value}}` was parsed but never used in the generated code | The step form isn't the one you think — check the pattern's exact syntax |
|
|
92
93
|
| `SG-W003` | `{ name }` filter defaulted from the Gherkin reference on a name-from-content role (toast/alert/status) | Add explicit `name:` or opt out with `name: null` |
|
|
94
|
+
| `SG-W004` | A selectors.yaml entry declares a **field no consumer reads**, so writing it changes nothing in the generated test. Reserved for this in the original diagnostics spec (`exact: true` on an entry with no construct it applies to) and implemented for the wider class by #712, where `pagination:` and `contenteditable:` were public, documented and plumbed — yet read by nothing, discoverable only by grepping. The field partition lives in `utils/selector-entry-fields.ts`, each entry naming WHO reads it. A warning, not an error: an unknown key is inert, so refusing to compile would break a project over a stray comment-turned-key while teaching nothing | Delete the field, or fix its spelling — the message names the nearest valid key. If the field genuinely should do something, that is a compiler gap: report it rather than leaving the declaration in place believing it works |
|
|
93
95
|
| `SG-W005` | `qa/app.yaml` unknown key / unknown preset / unparseable | Fix the key, or `extends: native\|radix\|antd` |
|
|
94
96
|
| `SG-W006` | `qa/app.<env>.yaml` found — unsupported by design | Describe the app once in `qa/app.yaml`; env differences belong in test-data overlays |
|
|
95
97
|
| `SG-W007` | Browser-alert handler step written AFTER an action — listener registers too late, the dialog auto-dismisses silently | Use the compound form `click [X] button and accept [OK] alert` (preferred), or move the alert step before the trigger |
|
|
@@ -98,10 +100,12 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
|
|
|
98
100
|
| `SG-W010` | A row step's `[Ref]` looks POSITIONAL (`First…`/`Last…`/`Top…`/`nth`) but has no selectors.yaml entry — the label is display-only, so the step checks existence ANYWHERE in the table; an ordering/sort-stability rule would pass even when the row moved | Declare the row in selectors.yaml (a `type: locator` value RELATIVE to the table, e.g. `first contact row:` → `value: 'tbody tr:first-child'`) — the step then asserts `toContainText` on that exact row. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
|
|
99
101
|
| `SG-W011` | A storage assertion written as `expect … in local/session storage` — `expect` steps only read `{{response}}` refs, so this matches no pattern. Compiles to a `throw` (was a silent no-op) | Rewrite using the supported grammar: `User see [KEY] in local storage exists/…` |
|
|
100
102
|
| `SG-W012` | A mock-install step written AFTER a navigation step in the same block — `page.route()` registered after `goto()` misses every request fired during page load | Move the mock-install step before the navigation, or into `Background` |
|
|
101
|
-
| `SG-W013` | A page
|
|
103
|
+
| `SG-W013` | A page step (`see [X] page` / `is on [X] page` / `wait for [X] page`) whose `[Ref]` has no `type: page` selector entry (or collides with a non-page entry) — the step falls back to the feature's own path (or `/<ref>/`) instead of `X`'s real URL, so the anchored match can never reach it: the assertion always fails, the wait can only time out | Declare a `type: page` entry for `[Ref]` with its real URL; if the key collides with another type, disambiguate with a `--type` suffix (`sungen-selector-keys` § Collision rule) |
|
|
102
104
|
| `SG-W014` | `[X] page with {{v}}` where `{{v}}`'s base test-data value carries no query and no fragment — the step checks the PATH only, asserting less than it reads as | Informational — pass a value like `?q=…` if you meant to assert a query, or drop `with {{v}}` for a bare page |
|
|
103
105
|
| `SG-W015` | `see [X] card/item with {{n}}` where `{{n}}`'s test-data value is a **bare number** — this compiles to a TEXT assertion (`toHaveText('3')`), NOT a count. An author reaching for "3 cards are shown" gets an assertion that can only pass if the card's TEXT is literally the digit: a wrong test with no error. Scoped to container-ish element types (`card`/`item`/`option` + plurals); a text element asserting a numeric readout (`[Row Quantity] text with 4`) stays silent | For a COUNT use a form that has count semantics: `see [Ref] row with {{n}}` / `see [T] table with {{n}}`, or a list `should have count`. If the number really is the element's text, assert it on a **text/label** element instead |
|
|
104
106
|
| `SG-W016` | A table step on a table located by CSS/testid whose value does not mention `table` and that declares no `rows:` — the table templates find rows by `getByRole('row')`; on a list built from `<div>`s that is 0 rows, so `is hidden`/`is empty` PASS without checking anything and counts/clicks time out (issue #688). A heuristic — the compiler cannot see the DOM — so it warns once per table | Declare the row shape on the TABLE entry (`rows: 'div.job-row'`, relative to the table) — it drives all seven table steps — or locate the table by `role: table` if it really is one. Do not put the shape on a row label's entry: that key positions ONE row |
|
|
107
|
+
| `SG-W017` | A step needs a **selector-entry field the entry does not declare**, so there is nothing honest to compile — today, a table paginate step whose table entry declares no `pagination.next`/`.prev` (#712). The mirror of `SG-W004`: that one reports a field written that nothing reads, this one a field a consumer needs that nobody wrote. The step compiles to a loud runtime `throw` rather than guessing a locator, which is the #690 failure mode (a locator no page has, failing far from its cause) | Add the missing field to the entry in selectors.yaml — for pagination: `pagination:` with `next:`/`prev:` locators. This is a selectors.yaml-only fix, allowed inside the run-test auto-fix loop |
|
|
108
|
+
| `SG-W018` | A selectors.yaml entry declares an **`nth:` outside the contract** — `nth: -2`, `nth: 'last'`, `nth: 1.5`. `nth:` was the one entry field whose VALUE nothing checked (issue #720), so the web adapter silently DROPPED the filter (identical to deleting the line — the step then hits `strict mode violation: resolved to N elements`, which names the DOM and never the YAML line) while the mobile adapter emitted `$$('~x')[-1]` / `[last]` — `undefined` indexing and a bare identifier. The value is now ignored and reported once per entry | Write an integer `>= 0` for a zero-based index, or `-1` for the last match (`.last()` on web, `.at(-1)` on mobile). There is no keyword form: `'last'` is a string, not an index. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
|
|
105
109
|
| `SG-E020` | A step matched a pattern, but the **active adapter ships no template** for it (e.g. a web-only step compiled under `platform: mobile`). The feature file still generates — that one step compiles to `throw new Error("[sungen] …")` naming the step, feature, pattern, template and adapter, so the failure is loud and traceable rather than a crashed build | Rephrase to a step the target adapter actually ships (see `sungen-gherkin-syntax` Platform Support section / `sungen-mobile-gestures`), or tag the scenario `@manual` with the platform reason |
|
|
106
110
|
| `SG-E021` | The step carries a trailing **state clause** (`… is hidden`) that the pattern which matched it does not assert — the clause would be SILENTLY DROPPED. Worst shape in this table: it does not weaken an assertion, it can INVERT one. `see [X] page is hidden` matched `page-assertion` (priority 13) ahead of `is-hidden` (11), so the step compiled to `toHaveURL(...)` — asserting the page IS displayed, and passing. Also fires on `wait for [X] is checked` (a wait honours only `hidden`/`visible`/`disabled`/`enabled`; everything else silently fell back to `visible`) and on `row`/`column in [T] table … is <state>` for any state but `hidden` | Move the state onto the element that has it: `Then User see [X] checkbox is checked`. For a page, assert the page you expect (`Then User see [Other] page`) or a marker element (`Then User see [Banner] is hidden`). If sungen cannot express the check, tag the scenario `@manual`. The diagnostic names the pattern that swallowed the clause — that name tells you which form you actually hit |
|
|
107
111
|
| `SG-W020` | The matched pattern **declares `platforms`** and the active platform isn't among them. Caught before template lookup, so the diagnostic names the alternative for the platform you are on. This is now the MAIN off-platform channel, not a corner case: the whole web-only set (`table-*`, `storage-*`, `route-assertion`, `tab-order`, `sorted`, `upload`, `alert-fill`) and the whole mobile-only set (gestures, device state, app lifecycle, device oracles) declare it, so the refusal arrives at compile time with a concrete substitute instead of at run time from inside a template. It still compiles to a `throw` — moving the message earlier never turns a refusal into a silent pass | Do what the message says: it carries the pattern's OWN `platformAlternative`, e.g. a table assertion on mobile → per-row accessibility-id assertions; `local storage` on mobile → assert the state via a UI element or `@api`/`@query`; a swipe on web → drive the control the swipe reveals. `@mock` on a mobile unit is the same shape (there is no Mock Driver on Appium) |
|
|
@@ -110,11 +114,18 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
|
|
|
110
114
|
| `SG-W023` | A mobile **setup** step that emitted NO executable code, so a declared PRECONDITION is never established. `Given the user is logged out` matches `user-not-logged-in` → the `clear-auth` template, which on Appium is a bare COMMENT: mobile auth lives in the keychain/prefs, not in cookies, so no per-step action can clear it. The scenario then runs against whatever the PREVIOUS scenario left signed in and passes regardless — the same false-pass class as `SG-W022`, arriving through setup instead of assertion. Deliberately NOT a `throw` (an unestablished precondition weakens setup, it does not INVERT an assertion) | Declare a feature-level reset tier — `@reset:data` on the Feature — so each scenario starts from a wiped data layer; app state is reset in the `beforeEach` hook, not mid-scenario by a step. For BACKEND state (`database is empty`) the remedy is different: reset through a test API/script, or bind a `@query:` precondition, the same way the web adapter has to |
|
|
111
115
|
| `SG-W024` | Two or more scenarios in one feature **share a name** and at least ONE carries a `@reset:` tag. The generated per-scenario tier lookup is keyed on the scenario NAME (a `@cases` title is built at runtime, so no compile-time key can match the rendered title), so every same-named scenario resolves to the SAME tier — including one that declared none, which then silently runs at its namesake's tier: a `@reset:none` chain broken, or a reinstall nobody asked for. Counted over EVERY scenario rather than the tagged subset, because the untagged twin is the one that gets the wrong tier and has no tag to be counted by | **Rename** the scenarios so each title is unique. Reported rather than resolved by precedence on purpose — two tests genuinely sharing one title cannot be told apart by a name-keyed lookup, so a rename is the only honest fix (silently picking a winner would be the same silent-discard shape as `SG-W021`) |
|
|
112
116
|
| `SG-W026` | A mobile **active-navigation** step that emitted NO executable code, so a promised screen change never happens. `When User open [Settings] page` compiles to the one-line `navigation` template — a COMMENT noting the app was already launched by the wdio capabilities — so the step reads as "go to Settings" and goes nowhere; pair it with a `page` assertion (itself a no-op, `SG-W022`) and the scenario passes without ever touching the app. Scoped to the ACTIVE forms only: `Given User is on [X] page|screen` is the app-LAUNCH anchor and correctly emits nothing, so it is never reported. Deliberately NOT a `throw` — an unfulfilled navigation weakens the scenario rather than INVERTING an assertion, and throwing would break every mobile suite already carrying one | Change screens the way a user does — `When User tap [Settings] button` — then assert a marker on the new screen (`Then User see [Settings Header] text`). Keep `is on [X] screen` only as the feature's opening anchor. Note `generate --strict` fails on any diagnostic, so this code is blocking there from the day it lands |
|
|
117
|
+
| `SG-W028` | A trailing **scope clause was DROPPED**, so the step runs against more of the page than its sentence names. Two causes, one code: (1) the word after `in [X]` is not a container type — `in [Job Card] widget`, or a leaf element used as a parent — so the clause matched nothing and the first-`[ref]`/first-`{{data}}` extractors took only the head of the sentence; (2) the by-content form `in [X] <type> with {{v}}` on the **mobile** adapter, where a parent is chained with `$()` and has no content filter. Before this code, both compiled clean and silently widened the assertion to the WHOLE screen — in a list of look-alike cards the test then passes for the wrong reason, findable only by diffing the generated `.spec.ts` against the Gherkin (#667) | For (1) use a real container type: `table` `list` `list-item` `section` `dialog` `modal` `drawer` `overlay` `form` `card` `item` `row` `group` `panel` `region` `container` `tab` `menu`. For (2) give the container an accessibility-id that identifies it and scope by that, or assert on the element the value belongs to |
|
|
118
|
+
| `SG-W029` | A **mobile feature with no usable app-package `Path:`** — the line is missing, or it holds a web-style route (`Path: /applied-jobs`). The compiler then cannot name the app to reset between scenarios, and before this code that was SILENT: the per-scenario `beforeEach` was not emitted at all, so Background fell to Mocha's `before()` — once per FILE. Every scenario shared one continuous app session, so a tab switch or a scroll in one stranded elements off-screen for every later one, and the `@reset:` tier ladder was dropped with it, leaving the tag inert (#669). The spec now always emits the reset machinery and resolves the app id from the session capability instead, throwing a named error at run time if nothing supplies one | Add the package to the feature: `Path: com.example.app/.MainActivity`, or `Path: <androidPkg>/<activity> | <iosBundleId>` for a dual-id feature. A `Path:` starting with `/` is a web route and will not be read as a package. Failing that, set `APP_PACKAGE` (Android) / `IOS_BUNDLE_ID` (iOS) so `wdio.conf` puts the id on the session |
|
|
113
119
|
| `SG-E022` | The **generated spec file itself** is not parseable JS/TS, so NOTHING in it runs — Playwright reports one `SyntaxError` and every scenario in the unit is lost. A compiler self-check (`@babel/parser`) on the emitted code, added after a field report where the runtime-data marker post-processor mis-paired quote delimiters across two literals and the user only found out at `npx playwright test`. Reported, never thrown — the file is still written so you keep the artifact to inspect. **Unlike every other code in this table this one blames the COMPILER, not your `.feature`** | Nothing to fix in the Gherkin — this should be impossible. **Report it** with the `.feature` (and the step) the file was generated from, attaching the unparseable `.spec.ts`. Check first that you are running the project-local compiler (`npx sungen`), not an older global one |
|
|
114
120
|
| `SG-E023` | The step carries an **assertion clause** (`matches "…"`, `contains`, `has text`) but the generated code is a BARE VISIBILITY check — the clause was swallowed by a catch-all and the test would pass whatever the element says. The sibling of `SG-E021`, for the clauses that are not states: `SG-E021` closed the state case declaratively, but a dropped `matches`/`contains`/`has text` still slid through (reproduced live: a `matches "<format>"` step compiled by an older sungen lost the assertion entirely, emitted no warning, and left a `toBeVisible()` that passed whatever the label said). Four separate defects arrived this way, so the step now compiles to a runtime `throw`. Deliberately narrow — fires only on the bare-visibility shape, so a pattern that genuinely consumed the clause (`is-hidden`, `contains`, `matches-format`, the state family, or a `filter({ hasText })` on a list member) is untouched | Either the phrasing is not the supported one for that clause — check the exact form in `sungen-gherkin-syntax` — or this sungen predates the step: verify you are running the project-local compiler (`npx sungen`), not an older global one |
|
|
115
121
|
| `SG-E024` | A mobile **app-lifecycle** step (`User restart the app` / `User reinstall the app`) in a feature that declares no `Path:` app id. The lifecycle templates resolve the target app through `__resolveAppId()`, a helper the appium test-file template emits ONLY when the id is present, so the spec used to compile clean and then die at runtime with `ReferenceError: __resolveAppId is not defined` — taking the WHOLE spec down, not the one step, because the failure lands in a before hook. (Field case: a mobile unit scaffolded with the nav recipe in `--path` instead of `--reach`, so `Path` never carried an app id.) Now refused at compile time as a `throw` naming the missing line | Add the app id under the `Feature` line: `Path: com.example.app/.MainActivity \| com.example.app`. For a mobile unit `--path` carries the **APP ID** and `--reach` carries the navigation recipe — if the nav recipe ended up in `--path`, that is the mistake |
|
|
122
|
+
| `SG-E025` | A feature tagged `@platform:ios` uses a step the Appium adapter does **not** serve on iOS — the template either throws deep inside a device run (`set-connectivity`, `location-services`, `open-notifications`) or silently does nothing (`hide-keyboard`, a guarded `if (!driver.isIOS …)`). Derived from the template body via the per-OS axis of the step-support matrix, so it cannot disagree with the adapter. Only an explicit `@platform:ios` is gated (`@platform:mobile` picks its OS at run time). Compiles to a loud `throw` | Use a step the matrix marks ✅ for iOS (see `docs/spec/step-support-matrix.md`), tag the feature `@platform:mobile` if it should run on either OS, or tag the scenario `@manual` |
|
|
123
|
+
| `SG-E026` | A **click step names a position** (`at position "top-left"`, `at offset x=…, y=…`) that would not be honoured: the keyword is misspelt or unquoted, a value is outside the grammar (not a number, negative, above 100%, a missing axis, an unknown preset), text follows the position, or another click form claimed the step first (`try to click`, a `with {{v}}` text filter, `… and accept [OK] alert`, `double click`). Each of those would compile to a click on the element's CENTRE and pass — for a backdrop-dismiss overlay that is the one outcome that proves nothing — so the step compiles to a loud `throw` | Write `click [X] <type> at position "top-left\|top-right\|bottom-left\|bottom-right"` or `… at offset x=<n>px\|%, y=<n>px\|%` as the LAST thing in the step, on a plain `click` (no `try to`, no `with {{v}}`, no alert clause). Web only — on mobile tap the control the position targets |
|
|
124
|
+
| `SG-E027` | A selectors.yaml entry declares a **`type:` that is not a selector type** — `type: 'testId'` (capital I), `'xpth'`, `'rol'`, `'test-id'`. YAML is untyped at run time, so this used to compile clean and resolve through the strategy switch's `default:` as `getByPlaceholder('<the value>')`: a locator that cannot match, surfacing much later as a timeout naming placeholder text nobody wrote. Reported once per entry, naming the entry key and the nearest valid spelling; every step using that entry now compiles to a `throw` | Fix the spelling in selectors.yaml. Valid types: `placeholder`, `role`, `testid`, `label`, `text`, `locator`, `id`, `upload`, `page`, `column`, `frame`, `table`, and the mobile strategies `accessibility-id`, `xpath`, `android-uiautomator`, `ios-predicate`, `css`. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
|
|
125
|
+
| `SG-W030` | A selectors.yaml **`expanded.class`/`expanded.attribute`** value carries a character a class or attribute name cannot — whitespace, a quote, a backslash, `$`, `{`, `}`. The value is pasted into the generated spec (string literal, regex, `classList` argument) and selectors.yaml is often generated from the page under test, so it is refused and the field dropped: the step falls back to driving and asserting `aria-expanded`. Reported once per entry and field. The checked/selected signal is refused outright instead of warned about, because dropping it leaves the step with no state to read at all | Use the plain class or attribute name (Tailwind arbitrary values such as `bg-[#eff6ff]` or `!mt-0` are accepted); never paste a selector fragment or quoted text into the field |
|
|
126
|
+
| `SG-E031` | A selectors.yaml **`checked:`/`selected:` declaration the compiler refuses**: the two spellings disagree (`checked.class` and `selected.class` with different values), a class token or attribute name carries whitespace / a quote / a backslash, or one entry declares both a `class` and an `attribute`. Every one of them is silent at run time — the generated matcher tests a single token, so the step would simply time out blaming your component — and an entry declaring two signals lets an action drive one while the assertion after it reads the other. Refused, not dropped: with no signal the step is back on the *"has no on/off state to set"* failure the declaration exists to remove | Declare ONE spelling and ONE of `class` / `attribute`, as a single plain token: `checked: { class: 'is-checked' }`, `selected: { class: 'border-link' }`, or `checked: { attribute: 'data-checked' }`. This is a selectors.yaml-only fix, allowed inside the run-test auto-fix loop |
|
|
116
127
|
|
|
117
|
-
A diagnostic never changes generation's own exit code — `sungen generate --strict` turns any collected diagnostic into a non-zero exit for CI gates. They are not all inert, though: **`SG-W001`, `SG-W011`, `SG-E020`, `SG-E021`, `SG-W020`, `SG-E023` and `SG-
|
|
128
|
+
A diagnostic never changes generation's own exit code — `sungen generate --strict` turns any collected diagnostic into a non-zero exit for CI gates. They are not all inert, though: **`SG-W001`, `SG-W011`, `SG-E020`, `SG-E021`, `SG-W020`, `SG-E023`, `SG-E024`, `SG-E025`, `SG-E026` and `SG-E027` additionally compile the offending step to a runtime `throw`, and `SG-E031` refuses the whole file** — a step that reads as a check and silently checks nothing is worse than one that fails loudly. Generation always COMPLETES: the rest of the scenario and the rest of the file still compile. `SG-E022` is the odd one out — it blames the compiler, not your `.feature`, and throws nothing (the file is written so you can inspect it).
|
|
118
129
|
|
|
119
130
|
### Runtime error → `Test data "<key>" references ${QA_*} but the environment variable is not set`
|
|
120
131
|
|
|
@@ -230,6 +241,7 @@ either direction.
|
|
|
230
241
|
| `Could not proxy command to the remote server` / `socket hang up` | `mobile-driver-proxy-lost` | session |
|
|
231
242
|
| `The instrumentation process cannot be initialized` | `mobile-instrumentation-failed` | session |
|
|
232
243
|
| `No Chromedriver found that can automate Chrome '…'` | `mobile-chromedriver-mismatch` | session |
|
|
244
|
+
| `[sungen] "switch to X frame" — no WEBVIEW/CHROMIUM context appeared within …ms` | `mobile-webview-context-absent` | session |
|
|
233
245
|
| `@auth:<role> login did not complete` / `could not decide the login state` | `mobile-auth-session` | session |
|
|
234
246
|
| `device log (logcat) did not contain "…"` | `mobile-device-log-silent` | oracle |
|
|
235
247
|
| `[sungen] … web-only` / `not supported on the appium adapter` | `mobile-web-only-step` | compile (SG-W020/E020) |
|
|
@@ -13,7 +13,8 @@ user-invocable: false
|
|
|
13
13
|
- **Actor**: Always `User`, always active voice.
|
|
14
14
|
- **Value**: `with {{snake_case}}` — never hardcode static data.
|
|
15
15
|
- **State**: `is <keyword>` — never use `{{}}` for states.
|
|
16
|
-
- **Parent scope**: `in [Parent] parentType` — optional, only when page has 2+ similar blocks needing disambiguation.
|
|
16
|
+
- **Parent scope**: `in [Parent] parentType` — optional, only when page has 2+ similar blocks needing disambiguation. Container types: `table` `list` `list-item` `section` `dialog` `modal` `drawer` `overlay` `form` `card` `item` `row` `group` `panel` `region` `container` `tab` `menu`. Anything else is not a container, and the clause is DROPPED with `SG-W028` rather than silently widening the step to the whole page.
|
|
17
|
+
- **Parent scope BY CONTENT** (web only): `… with {{v}} in [Parent] parentType with {{v2}}` — the SECOND `with` picks the one container whose content is `{{v2}}`, for a list of look-alike cards/rows. One `with` after the clause still belongs to the TARGET (`see [first] row in [apply list] table with {{job_name}}`), so the by-content form always carries two. On the mobile adapter it reports `SG-W028` — an Appium parent chain has no content filter.
|
|
17
18
|
|
|
18
19
|
## Keyword → Action Rules
|
|
19
20
|
|
|
@@ -45,6 +46,8 @@ User select [T] dropdown with {{v}}
|
|
|
45
46
|
User click [T] button | tab | column | breadcrumb
|
|
46
47
|
User click [T] row with {{v}}
|
|
47
48
|
User try to click [T] button | link # DISABLED element only — see rule below (v3.3) [web]
|
|
49
|
+
User click [T] overlay at position "top-left" # corner preset: top-left | top-right | bottom-left | bottom-right [web]
|
|
50
|
+
User click [T] overlay at offset x=24px, y=10% # px or % per axis, from the element's top-left [web]
|
|
48
51
|
User double click [T] element
|
|
49
52
|
User hover [T] icon | row # no-op on mobile (see Platform Support)
|
|
50
53
|
User drag [T] to [T2]
|
|
@@ -57,7 +60,17 @@ User expand | collapse [T] row
|
|
|
57
60
|
scenario asserts nothing happens (cooldown re-send, submit before the form is valid). `click` waits for
|
|
58
61
|
`enabled` → it can only time out there; `try to click` compiles to `.click({ force: true })`. Always
|
|
59
62
|
pair it with the proof: `Then User see [T] TYPE is disabled` + an assertion that the effect did NOT occur.
|
|
60
|
-
NEVER use it for a click that is supposed to work — it deletes the actionability wait.
|
|
63
|
+
NEVER use it for a click that is supposed to work — it deletes the actionability wait. (`force: true` is
|
|
64
|
+
reachable ONLY through `try to click` — there is no separate `force` keyword.)
|
|
65
|
+
|
|
66
|
+
**click at position rule** (web only): a plain `click` lands on the element's CENTRE. To click a
|
|
67
|
+
modal backdrop where the dialog is not, or any control whose centre is covered, append
|
|
68
|
+
`at position "<corner>"` (5% in from that corner) or `at offset x=<n>px|%, y=<n>px|%`. The position
|
|
69
|
+
is checked against the element's real box at run time (an offset past it fails naming the size). The
|
|
70
|
+
suffix is refused at compile time (`SG-E026`, the step throws instead of clicking the centre) when it
|
|
71
|
+
is misspelt or unquoted, when a value is negative / above 100% / missing an axis, when anything
|
|
72
|
+
follows it, and when it is combined with `try to click`, a `with {{v}}` text filter, `… and accept [OK]
|
|
73
|
+
alert`, or `double click` — those forms take no position.
|
|
61
74
|
|
|
62
75
|
### Alert / Keyboard / Wait / Scroll
|
|
63
76
|
|
|
@@ -68,12 +81,11 @@ User click [OK | Cancel] alert # two-step form: must come BEF
|
|
|
68
81
|
User fill [T] alert with {{v}} # no-op on mobile — native prompt fill is app-specific
|
|
69
82
|
User see [message text] alert
|
|
70
83
|
User press Escape key | [Enter] key | Tab key 5 times | Enter on [T] field
|
|
71
|
-
User wait for N seconds | [T] page # [T] page: web waits for the URL; mobile pauses (settle) — see Platform Support
|
|
84
|
+
User wait for N seconds | [T] page # [T] page: web waits for the URL, then HOLDS it (see Pattern 8); mobile pauses (settle) — see Platform Support
|
|
72
85
|
User wait for [T] TYPE is visible | hidden | enabled | disabled # ANY reference (v3.3)
|
|
73
86
|
User wait for [T] TYPE with {{v}} # until it shows the value
|
|
74
87
|
User wait for [T] table to refresh # filter/search/pagination round-trip (v3.3)
|
|
75
88
|
User scroll to [T] section
|
|
76
|
-
User switch to [T] frame | [main] frame # web: iframe; mobile: hybrid-app WebView context (no-op if the screen has no WebView)
|
|
77
89
|
```
|
|
78
90
|
|
|
79
91
|
> **Browser alerts (native `window.confirm/alert/prompt` only):** prefer the compound form —
|
|
@@ -86,6 +98,36 @@ User switch to [T] frame | [main] frame # web: iframe; mobile: hybrid-
|
|
|
86
98
|
> `wait for N seconds` stays a last resort. `table to refresh` watches the app's loading
|
|
87
99
|
> indicator (`qa/app.yaml` `feedback.loading.indicator`, default `[aria-busy="true"]`).
|
|
88
100
|
|
|
101
|
+
### Scope control
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
User is on [T] dialog # ENTER dialog scope, rooted at [T]'s selectors.yaml entry
|
|
105
|
+
User use dialog # ENTER dialog scope, unnamed — plain page.getByRole('dialog')
|
|
106
|
+
User close dialog | dismiss dialog | exit dialog # EXIT dialog scope (three spellings, identical)
|
|
107
|
+
User switch to [T] frame # ENTER frame scope — web: iframe; mobile: hybrid-app WebView context
|
|
108
|
+
User switch to [main] frame # EXIT frame scope (no-op if the screen has no WebView)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> **Dialog scope is STICKY — it ends only where you end it.** Once a scenario enters dialog scope,
|
|
112
|
+
> EVERY later locator in that scenario is prefixed with the dialog root, **including the steps that
|
|
113
|
+
> run after the dialog has closed**. Nothing implicit drops it: not a click on the dialog's own
|
|
114
|
+
> Close/OK button, and **not** `wait for [T] dialog is hidden` — that step asserts the dialog went
|
|
115
|
+
> away and then keeps resolving the following steps INSIDE it, which can never succeed. Only
|
|
116
|
+
> `close`/`dismiss`/`exit dialog`, or the start of the next scenario, drops the scope. The failure
|
|
117
|
+
> has a recognisable shape: a step after the dialog closed times out reading "element not found"
|
|
118
|
+
> while the element is plainly on screen — you are not missing a selector, you are still scoped
|
|
119
|
+
> inside a dead dialog.
|
|
120
|
+
>
|
|
121
|
+
> ```gherkin
|
|
122
|
+
> Given User is on [Confirm Delete] dialog # scope enters here — implicitly, but it still needs an EXPLICIT exit
|
|
123
|
+
> When User click [OK] button # scoped — correct, the button IS in the dialog
|
|
124
|
+
> And User exit dialog # ← REQUIRED: the click closed the dialog, the scope did not
|
|
125
|
+
> Then User see [Deleted] toast # page-rooted again
|
|
126
|
+
> ```
|
|
127
|
+
>
|
|
128
|
+
> Dialog scope is `[web]`-effective only — no Appium template reads `inDialog`, so on mobile the
|
|
129
|
+
> following steps resolve against the whole screen (`SG-W021`). Frame scope works on both.
|
|
130
|
+
|
|
89
131
|
### Positional table rows (v3.3) `[web]`
|
|
90
132
|
|
|
91
133
|
```
|
|
@@ -142,6 +184,20 @@ detail:
|
|
|
142
184
|
anchor: { type: 'role', value: 'heading', name: 'Item Detail' } # the rendered-content proof
|
|
143
185
|
```
|
|
144
186
|
|
|
187
|
+
**`wait for [T] page` shares that hold, and only that.** Since 3.2.30 the wait compiles to
|
|
188
|
+
`waitForPage(page, {…})` on the same helper: it arrives, settles, holds the URL, and fails naming
|
|
189
|
+
where the user went — so the steps AFTER the wait no longer run against a page mid-redirect, and
|
|
190
|
+
`/vi/search` no longer resolves on `/vi/search-results`. Two differences from the `Then` side:
|
|
191
|
+
|
|
192
|
+
- **A wait does not constrain the query.** `see [Results] page` with no `with {{v}}` demands a bare
|
|
193
|
+
URL; `wait for [Results] page` after submitting a search tolerates the `?q=…` it lands on. Prove
|
|
194
|
+
the query in the `Then` that follows.
|
|
195
|
+
- **A wait is not an oracle.** It never counts toward a scenario's assertions — `ORACLE-URL-ONLY`
|
|
196
|
+
and the assertionless-test check both ignore it. Always follow it with a real `Then`.
|
|
197
|
+
|
|
198
|
+
An absolute page `value` (`https://app.example.com/reception/:id`) compiles its host into a
|
|
199
|
+
separate anchored `hostname:` — the same path on another host no longer satisfies the wait.
|
|
200
|
+
|
|
145
201
|
**Pattern 8 — the page assertion judges pathname AND query together.** Both `see [T] page` and
|
|
146
202
|
`is on [T] page` (Then-side) compile to ONE web-first check — `expectPage(page, {…})`, built on
|
|
147
203
|
`toHaveURL(urlMatches({…}))` and retrying like any other assertion — comparing the **exact** pathname
|
|
@@ -211,6 +267,15 @@ User see [Table] table match data:
|
|
|
211
267
|
|
|
212
268
|
Row scope: `see [Ref] row in [Table] table with {{v}}` enters scope. Subsequent `see [Col] column with {{v}}` checks cell in that row. Use `table match data:` for multi-row verification.
|
|
213
269
|
|
|
270
|
+
**[Col] does not have to be the DOM header text.** `see [Col] column in [T] table` looks for a header whose accessible name is the reference, which cannot match a page in another language. Keep the reference readable — it is what the delivered test case shows — and declare the real header on the table entry (or on a `<col>--columnheader` entry's `name:`, which also takes `{{variables}}` for per-locale text):
|
|
271
|
+
```yaml
|
|
272
|
+
article list:
|
|
273
|
+
type: role
|
|
274
|
+
value: table
|
|
275
|
+
columns:
|
|
276
|
+
category: { index: 2, header: '記事分類' }
|
|
277
|
+
```
|
|
278
|
+
|
|
214
279
|
**[Ref] of a row step is a display label UNLESS declared in selectors.yaml.** Without an entry the step checks the value exists in ANY row — a name like `[First Contact Row]` does NOT position the row, and an ordering rule would pass even when the row moved (SG-W010 warns on positional-looking undeclared labels). To assert a POSITION, declare the row (a `type: locator` value RELATIVE to the table):
|
|
215
280
|
```yaml
|
|
216
281
|
first contact row:
|
|
@@ -402,7 +467,7 @@ Full syntax → `sungen-mobile-gestures`.
|
|
|
402
467
|
|---|---|---|
|
|
403
468
|
| `see [T] page` \| `page with {{v}}` | asserts path+query | **silent no-op** — asserts nothing; the scenario passes regardless. Assert a screen-marker element instead |
|
|
404
469
|
| `is on [T] page` \| `open [T] page` (Given/When) | navigates via URL | `is on` = the app-launch anchor, correctly a no-op. `open`/`navigate to` = a no-op that **reports `SG-W026`** — it promises a screen change and performs none; use tap/gesture steps instead |
|
|
405
|
-
| `wait for [T] page` | waits for the URL | fixed `driver.pause(500)` settle — not a real wait condition |
|
|
470
|
+
| `wait for [T] page` | waits for the URL, then settles and HOLDS it (`waitForPage`) | fixed `driver.pause(500)` settle — not a real wait condition |
|
|
406
471
|
| `go back` (OS/hardware back) | **not a web step** — reports `SG-W020`; re-enter with `is on [X] page` or tag `@manual` | Android: `KEYCODE_BACK`, the real hardware key. iOS: WDA **navigation**-back, *not* the user's edge swipe-back (iOS has no hardware back). Distinct from `tap [Back] button`: system back skips the screen's own handler unless the app wires it |
|
|
407
472
|
| `hover [T] icon \| row` | real hover | no-op — hover-revealed content is normally already visible on mobile; use `tap` |
|
|
408
473
|
| `fill [T] alert with {{v}}` | fills native `prompt()` | no-op (comment only) — app-specific, handle manually |
|
|
@@ -492,7 +557,17 @@ award:
|
|
|
492
557
|
| `upload` | — | — | file inputs |
|
|
493
558
|
| `frame` | iframe selector | — | iframes |
|
|
494
559
|
|
|
495
|
-
Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `columns`
|
|
560
|
+
Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `columns` `checked` `selected`
|
|
561
|
+
|
|
562
|
+
`checked: { class: <cls> }` or `checked: { attribute: <attr> }` — how a checkbox-like control that is
|
|
563
|
+
NOT a real checkbox (a `<div>` tile with no `role`/`aria-checked`; `.check()` throws "Not a checkbox or
|
|
564
|
+
radio button") shows its state. `selected:` is an ALIAS of the same signal, for a card/tab picker where
|
|
565
|
+
`checked:` reads wrong; declare ONE spelling and ONE of class/attribute. `check`/`uncheck` click only
|
|
566
|
+
when the state differs, then assert it; `is checked` / `is unchecked` / `is selected` (plain or
|
|
567
|
+
`with {{v}}`) assert it. The class is read on the located element OR on a descendant (a checkmark
|
|
568
|
+
`<svg>` that only mounts when on); the attribute is read on the located element itself. Other states on
|
|
569
|
+
the same entry (`is focused`, …) keep their native matchers. The value must be ONE plain class token or
|
|
570
|
+
attribute name — whitespace, a quote or a backslash is refused at compile time, naming the entry.
|
|
496
571
|
|
|
497
572
|
`scope` (e.g. `scope: dialog`) is `[web]`-effective only — no Appium template reads `inDialog`, so
|
|
498
573
|
on mobile a dialog-scoped ref still resolves against the whole screen (`SG-W021`).
|
|
@@ -809,7 +884,32 @@ Only when `@cleanup:*` tags aren't enough — feature-specific logic.
|
|
|
809
884
|
|
|
810
885
|
### Layer 3: `@beforeAll` / `@afterAll` (optional)
|
|
811
886
|
|
|
812
|
-
For one-time setup/teardown.
|
|
887
|
+
For one-time setup/teardown. A hook scenario takes `@api:` / `@query:` preconditions exactly like an
|
|
888
|
+
ordinary scenario (override args and `@concurrent:N` included) — they run inside the hook and bind
|
|
889
|
+
`{{name}}`:
|
|
890
|
+
|
|
891
|
+
```gherkin
|
|
892
|
+
@beforeAll @api:login(email={{email}},password={{pw}}) @api:list_items(token={{login.body.token}})
|
|
893
|
+
Scenario: seed the session once before any scenario
|
|
894
|
+
Then expect {{login.status}} is 200
|
|
895
|
+
And expect {{list_items.status}} is 200
|
|
896
|
+
|
|
897
|
+
@afterAll @api:cleanup
|
|
898
|
+
Scenario: drop the seeded data after the last scenario
|
|
899
|
+
Then expect {{cleanup.status}} is 204
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
- A variable bound in `@beforeAll` (`{{login.body.token}}`) is readable from EVERY scenario of the
|
|
903
|
+
feature (it runs before each of them). `@afterEach`/`@afterAll` bindings stay hook-local.
|
|
904
|
+
- A failing hook skips every test in the file, so hook steps are COLLECTED: each `Then` that fails is
|
|
905
|
+
recorded and the next step still runs; a failing action step (`Given`/`When`) stops the hook there.
|
|
906
|
+
The hook fails ONCE with the full list (`Hook preconditions not met`), not with the first failure.
|
|
907
|
+
- `@cases` and `@extend` are not supported on a hook — they are ignored with a compile warning.
|
|
908
|
+
- A hook has no `page` fixture on a per-scenario (non-`@serial`) feature — keep its steps to `@api:`/
|
|
909
|
+
`@query:` preconditions and `expect` assertions there.
|
|
813
910
|
|
|
814
911
|
**Rendering order in `.spec.ts`:**
|
|
815
|
-
`test.describe` → `test.use(storageState)` → `test.use(autoCleanup)` → `test.beforeAll` → `test.beforeEach` → `test.afterEach` → `test.afterAll` → `test()` blocks
|
|
912
|
+
`test.describe` → `test.use(storageState)` → `test.use(autoCleanup)` → `test.beforeAll` → `test.beforeEach` → `test.afterEach` → `test.afterAll` → `test()` blocks.
|
|
913
|
+
On a `@serial` feature the user `@beforeAll`/`@afterAll` blocks are registered right after the built-in
|
|
914
|
+
`beforeAll` (page creation) and BEFORE the built-in `afterAll` (page close): Playwright runs `afterAll`
|
|
915
|
+
hooks in registration order, so the shared `page` is still open for the user's teardown steps.
|
|
@@ -96,6 +96,7 @@ When running Phase 0 for a **flow** (`qa/flows/<name>/`), check existing screen
|
|
|
96
96
|
- Toast/alert named from content (no aria-label) → `feedback.toast: { role: status, named_by: content }` + `name: null` on the selector entries.
|
|
97
97
|
- Loading indicator for `wait for [T] table to refresh` → `feedback.loading.indicator` (skip if the app sets `aria-busy`).
|
|
98
98
|
- **Every qa/app.yaml edit MUST carry provenance** in `detection:`: who/when (`detected_by`) and the observed evidence per key (`evidence:`). A profile without evidence is a guess a human cannot review.
|
|
99
|
+
7c. **Checkbox-like control that is not a checkbox** (`.check()` failed with `Not a checkbox or radio button`; the snapshot shows a `<div>`/`<button>` tile with no `role="checkbox"` and no `aria-checked`): keep the locator that finds the tile and add the state signal to the SAME entry — `checked: { class: '<class present when on>' }` (a class on that element itself, e.g. `bg-selected`) or `checked: { attribute: '<attr>' }` (a `'true'`/`'false'` attribute such as `data-checked`); on a card/tab picker write `selected:` instead — same signal, different spelling. This is what makes `check`/`uncheck`/`is checked`/`is selected` compile for the tile. ONE plain class token or attribute name only — whitespace or a quoted fragment is refused at compile time, naming the entry. Same shape as `expanded:` for accordions; `checked.class` matches on the located element OR a descendant carrying it (the checkmark icon), while `checked.attribute` is read on the located element itself.
|
|
99
100
|
8. **Merge, don't overwrite**: preserve the page selector and any user-authored entries in `selectors.yaml`. Only add missing keys.
|
|
100
101
|
- **Type-collision check** (see `sungen-selector-keys` § Collision rule): before writing a base key, check whether it already exists with a *different* `type`. If so, give **both** entries a `--type` suffix instead of clobbering or sharing the base key — a base-key entry of the wrong type gets picked up by any step referencing that label. Watch `page` especially: a `[X] page` step reads `value` as a URL, so if `X` already holds a button (`value: 'button'`) the compiler won't use the button's value as a URL, but it can't invent the real one either — it falls back to the feature's own path and warns `SG-W013`, asserting a page that isn't `X`'s. Write `X--page` (URL from the live page / feature `Path:`) alongside `X--button`.
|
|
101
102
|
9. **Show summary + confirm**: list the keys that will be added, ask the user to approve, then write the file.
|
|
@@ -219,7 +219,7 @@ If no YAML key exists, the resolver infers from the Gherkin element type:
|
|
|
219
219
|
| `[X] button` | `getByRole('button', { name: 'X' })` | Button's accessible name = X |
|
|
220
220
|
| `[X] link` | `getByRole('link', { name: 'X' })` | Link text = X |
|
|
221
221
|
| `[X] heading` / `header` | `getByRole('heading', { name: 'X' })` | Heading text = X |
|
|
222
|
-
| `[X] checkbox` | `getByRole('checkbox', { name: 'X' })` | Checkbox label = X |
|
|
222
|
+
| `[X] checkbox` | `getByRole('checkbox', { name: 'X' })` | Checkbox label = X — a checkbox-LIKE tile with no `role`/`aria-checked` needs an explicit entry plus `checked: { class: <cls> }` or `checked: { attribute: <attr> }` (read on the located element itself; `expanded.class` reads a descendant) |
|
|
223
223
|
| `[X] radio` | `getByRole('radio', { name: 'X' })` | Radio label = X |
|
|
224
224
|
| `[X] field` | `getByPlaceholder('X')` | Placeholder text = X AND field has a placeholder |
|
|
225
225
|
| `[X] text` / `message` / `label` | `getByText('X')` | Visible text = X (partial match) |
|
|
@@ -231,7 +231,7 @@ If no YAML key exists, the resolver infers from the Gherkin element type:
|
|
|
231
231
|
| `[X] tab` | `getByRole('tab', { name: 'X' })` | Tab text = X |
|
|
232
232
|
| `[X] table` | `getByRole('table', { name: 'X' })` | Table aria-label = X |
|
|
233
233
|
| `[X] list` | `getByRole('list', { name: 'X' })` | List aria-label = X |
|
|
234
|
-
| `[X] column` | `getByRole('columnheader', { name: 'X' })` | Column header text = X |
|
|
234
|
+
| `[X] column` | `getByRole('columnheader', { name: 'X' })` | Column header text = X — **overridable**, see the column-header note below |
|
|
235
235
|
| `[X] dialog` / `modal` / `drawer` | `getByRole('dialog', { name: 'X' })` | Dialog aria-label/heading = X |
|
|
236
236
|
| `[X] dropdown` / `select` | `getByRole('combobox', { name: 'X' })` | Combobox label = X |
|
|
237
237
|
| `[X] menuitem` | `getByRole('menuitem', { name: 'X' })` | Menu item text = X |
|
|
@@ -244,6 +244,8 @@ If no YAML key exists, the resolver infers from the Gherkin element type:
|
|
|
244
244
|
| `[X] breadcrumb` | `getByRole('navigation', { name: 'X' })` | Navigation aria-label = X |
|
|
245
245
|
| `[X] badge` / `tooltip` / `tag` | `getByText('X')` | Visible text = X |
|
|
246
246
|
|
|
247
|
+
**Column headers: the `[Reference]` is not required to be the DOM text.** `[X] column` defaults its accessible name to the reference, which is unmatchable when the page is not in the reference's language (a `[Category]` column whose header reads 記事分類). Declare the real header instead of renaming the reference — the reference is what ships in the delivered test case. Most specific first: the table entry's `columns.<col>.header`, then a `<col>--columnheader` entry's `name:`, then the reference text. A `header` may hold `{{variables}}` for per-locale text, like any `name:`. A same-named entry that is NOT a column header is ignored and cannot hijack the lookup.
|
|
248
|
+
|
|
247
249
|
**Special note on the list members `[X] item` / `[X] option` / `[X] cell`** — the only rows above that behave differently per platform:
|
|
248
250
|
|
|
249
251
|
- **Web**: they infer the role with **no `name`**, on purpose. A list member takes its accessible name from its own CONTENT, so a name defaulted from the `[Reference]` text matches zero elements — and `toBeHidden()` PASSES on an empty locator, so the scenario would go green having proved nothing. The role alone is the whole locator; `nth` or a count assertion narrows it. An explicit `name:` you write yourself is always honoured.
|
|
@@ -255,6 +257,11 @@ If no YAML key exists, the resolver infers from the Gherkin element type:
|
|
|
255
257
|
|
|
256
258
|
**Only add a YAML entry when** auto-infer cannot work: DOM name differs from Gherkin label, need `testid`, need `nth`, need `exact: true`, need `name: null`, or the field type requires explicit config.
|
|
257
259
|
|
|
260
|
+
**`nth:` takes an integer `>= -1`** — `0`, `1`, `2`… are zero-based indexes and `-1` is the LAST
|
|
261
|
+
match (`.last()` on web, `.at(-1)` on mobile). Nothing else is an index: `nth: -2`, `nth: 'last'`
|
|
262
|
+
and `nth: 1.5` are ignored, the selector stops filtering by position, and the compiler reports
|
|
263
|
+
`SG-W018`. Reach for `nth` only when `exact: true` or `scope` cannot disambiguate.
|
|
264
|
+
|
|
258
265
|
### `name: null` — name-from-content roles (toast, alert, spinner)
|
|
259
266
|
|
|
260
267
|
For a `type: role` entry **without** `name:`, the compiler defaults the accessible-name filter to the Gherkin `[Reference]` text. That is right for buttons/links — and **always wrong** for roles whose accessible name derives from their CONTENT (`status`, `alert`, `log`, `marquee`, `timer`): a toast's name is its message text, never the string "Success Toast". The compiler warns (`SG-W003`). Opt out explicitly:
|
|
@@ -97,7 +97,7 @@ Hard-fail gate. Any ✗ is a syntax error — must be fixed; do not score furthe
|
|
|
97
97
|
|
|
98
98
|
- [ ] Selector keys: lowercase, keep Unicode, use **spaces** (no underscores/dots); same label → `--type`/`--N` suffix; names >30 chars → shorten to 1–3 words
|
|
99
99
|
- [ ] Types requiring explicit YAML: `date-picker`, `uploader`, `overlay`, `frame`, `step`
|
|
100
|
-
- [ ] Every `{{var}}` in `.feature` exists in `test-data.yaml
|
|
100
|
+
- [ ] Every `{{var}}` in `.feature` exists in `test-data.yaml`, and every test-data key is bound by some scenario. Both directions are checked by the advisory **data-driven lint** printed at the end of `sungen generate` (warn, never blocking; it is NOT an `audit` finding): **forward** — `{{x}} is not a test-data key and nothing binds it at run time` (the compiler does not fail; the step compiles to the literal text `'${x}'` and runs green having tested nothing, so treat this warning as a defect); **reverse** — `test-data key "x" is defined but never referenced` (dead data). Not counted as unbound/orphan: `{{$…}}` dynamic vars, runtime-bound vars (`remember … as {{v}}`, the scenario's own `@api:`/`@query:` names, a `@beforeAll` hook's `@api:`/`@query:` bindings, a `@steps:` base's captures for its `@extend`ers, and a `@query`/`@api` param bound by name with no override), `@cases` dataset columns, `_*` factory metadata keys, `{{var}}` used in `selectors.yaml` (i18n names) or `mock/mocks.yaml` (mock bodies), and `{{var}}` inside a `@manual` scenario's `# Tester verifies:` comments
|
|
101
101
|
- [ ] Dynamic vars (`{{$timestamp}}`/`{{$uuid}}`/`{{$random:a:b}}`) used correctly for run-unique data (CRUD)
|
|
102
102
|
- [ ] `locator` (CSS) only as a last resort
|
|
103
103
|
|
|
@@ -291,7 +291,7 @@ Do NOT deduct points when a `.feature` lacks the following viewpoints (Playwrigh
|
|
|
291
291
|
1. ID unique + correct format `VP-<CAT>-<NNN>`?
|
|
292
292
|
2. Title understandable immediately, no context needed?
|
|
293
293
|
3. Every step uses the right keyword (`Given`=is on / `When`=action / `Then`=see)?
|
|
294
|
-
4. Every `{{var}}` exists in `test-data.yaml
|
|
294
|
+
4. Every `{{var}}` exists in `test-data.yaml`, and every test-data key is bound by some scenario? (`sungen generate` ends with a data-driven lint warning for either direction — an unbound `{{x}}` compiles to the literal placeholder, it does not fail)
|
|
295
295
|
5. At least one happy path?
|
|
296
296
|
6. Negative cases for invalid classes (EP)?
|
|
297
297
|
7. Boundary covers the 4 points (`min-1/min/max/max+1`)?
|
|
@@ -318,7 +318,7 @@ Do NOT deduct points when a `.feature` lacks the following viewpoints (Playwrigh
|
|
|
318
318
|
9. **Missing negative path** → map invalid classes, add scenarios.
|
|
319
319
|
10. **All `@high`** → reset by user impact per the `sungen-gherkin-syntax` priority table (auth/CRUD/security/required → `@high`; format/standard-range boundary/search → `@normal`; cosmetic → `@low`).
|
|
320
320
|
11. **`@manual` without reason** → add a technical reason comment explaining why automation is not possible.
|
|
321
|
-
12. **Orphan
|
|
321
|
+
12. **Orphan key in test-data** (the advisory lint after `sungen generate` says `defined but never referenced`; a `@query`/`@api` param bound by name, a `@manual` comment ref or a `_*` metadata key is not one) → delete it or bind it into a scenario. **Unbound `{{x}}` in a step** (the same lint says `is not a test-data key and nothing binds it`) → add the key or fix the ref — left alone it runs as the literal text `${x}`.
|
|
322
322
|
13. **Unnecessary CSS/locator selector** → switch to role+name/label/text; keys use spaces.
|
|
323
323
|
14. **Scope crammed into `Background`** → split into `@steps`/`@extend`.
|
|
324
324
|
15. **Mixed auth groups missing `@parallel`** → add `@parallel`.
|
|
@@ -361,7 +361,7 @@ If `selectors/<name>.yaml` exists, count lines matching `@needs-live-verify` (pr
|
|
|
361
361
|
- [ ] Keyword→Action correct
|
|
362
362
|
- [ ] Action→Type correct; click-rule correct
|
|
363
363
|
- [ ] Assertions follow the 8 patterns, no redundant `is visible`
|
|
364
|
-
- [ ] Every {{var}} exists in test-data.yaml
|
|
364
|
+
- [ ] Every {{var}} exists in test-data.yaml and no orphan keys — both reported by the advisory data-driven lint at `sungen generate` (not `audit`); an unbound {{var}} compiles to a literal placeholder, never a compile error
|
|
365
365
|
- [ ] Selector keys follow sungen-selector-keys; locator only as last resort
|
|
366
366
|
- [ ] Tags valid; @parallel when mixing auth groups
|
|
367
367
|
- Syntax errors found: <n>
|
|
@@ -84,9 +84,16 @@ export default defineConfig({
|
|
|
84
84
|
/* Output file path is controlled by PLAYWRIGHT_JSON_OUTPUT_NAME env var for per-screen isolation. */
|
|
85
85
|
/* When SUNGEN_ENV is set, the env name is inserted before `.json` so locale */
|
|
86
86
|
/* runs don't overwrite each other (e.g. `<name>-test-result.vi.json`). */
|
|
87
|
+
/* The sanitize reporter rewrites the JSON report's absolute machine paths to project-relative ones on
|
|
88
|
+
exit — the file is committed next to the spec, so a username or machine layout must not ship in it.
|
|
89
|
+
Registered only while its file exists (`sungen init`/`generate`/`update` write it): a cleaned
|
|
90
|
+
specs/generated must not stop Playwright from loading this config. */
|
|
87
91
|
reporter: [
|
|
88
92
|
['html'],
|
|
89
93
|
['json', { outputFile: resolveJsonOutputFile() }],
|
|
94
|
+
...(fs.existsSync(path.join(__dirname, 'specs', 'generated', 'report-path-sanitize-reporter.ts'))
|
|
95
|
+
? [['./specs/generated/report-path-sanitize-reporter.ts'] as [string]]
|
|
96
|
+
: []),
|
|
90
97
|
],
|
|
91
98
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
92
99
|
use: {
|