@sun-asterisk/sungen 3.2.30 → 3.2.32
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/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +3 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +11 -0
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/setup-mobile-env.d.ts +15 -0
- package/dist/cli/commands/setup-mobile-env.d.ts.map +1 -0
- package/dist/cli/commands/setup-mobile-env.js +65 -0
- package/dist/cli/commands/setup-mobile-env.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +26 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +13 -2
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +56 -13
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +39 -2
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +38 -1
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +1 -1
- package/dist/generators/test-generator/patterns/index.js +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +35 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +103 -4
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +18 -0
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +47 -0
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +17 -3
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +23 -4
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +2 -1
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +42 -7
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/spec-branches.d.ts +7 -2
- package/dist/harness/spec-branches.d.ts.map +1 -1
- package/dist/harness/spec-branches.js +62 -11
- package/dist/harness/spec-branches.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +16 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +119 -10
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/orchestrator/appium-environment-setup.d.ts +24 -0
- package/dist/orchestrator/appium-environment-setup.d.ts.map +1 -0
- package/dist/orchestrator/appium-environment-setup.js +157 -0
- package/dist/orchestrator/appium-environment-setup.js.map +1 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +2 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +1 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +14 -0
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +55 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
- package/dist/orchestrator/templates/appium-environment/README.md +83 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
- package/dist/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
- package/dist/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
- package/dist/orchestrator/templates/appium-environment/setup.ps1 +58 -0
- package/dist/orchestrator/templates/appium-environment/setup.sh +88 -0
- package/dist/orchestrator/templates/specs-checked-state.d.ts +1 -1
- package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-checked-state.js +26 -3
- package/dist/orchestrator/templates/specs-checked-state.js.map +1 -1
- package/dist/orchestrator/templates/specs-checked-state.ts +31 -6
- package/dist/orchestrator/templates/specs-mock.d.ts +2 -1
- package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-mock.js +10 -3
- package/dist/orchestrator/templates/specs-mock.js.map +1 -1
- package/dist/orchestrator/templates/specs-mock.ts +12 -4
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +2 -1
- package/src/cli/commands/capability.ts +12 -1
- package/src/cli/commands/init.ts +3 -0
- package/src/cli/commands/setup-mobile-env.ts +64 -0
- package/src/cli/index.ts +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +26 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +13 -2
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
- package/src/generators/test-generator/code-generator.ts +66 -17
- package/src/generators/test-generator/diagnostics.ts +39 -2
- package/src/generators/test-generator/patterns/index.ts +1 -1
- package/src/generators/test-generator/step-mapper.ts +106 -5
- package/src/generators/test-generator/template-engine.ts +18 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +18 -3
- package/src/harness/audit.ts +24 -5
- package/src/harness/next-step.ts +2 -1
- package/src/harness/parse.ts +42 -7
- package/src/harness/spec-branches.ts +67 -12
- package/src/harness/spec-coverage.ts +121 -11
- package/src/orchestrator/appium-environment-setup.ts +127 -0
- package/src/orchestrator/mobile-runtime-scaffolder.ts +2 -0
- package/src/orchestrator/project-initializer.ts +15 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +55 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
- package/src/orchestrator/templates/appium-environment/README.md +83 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
- package/src/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
- package/src/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
- package/src/orchestrator/templates/appium-environment/setup.ps1 +58 -0
- package/src/orchestrator/templates/appium-environment/setup.sh +88 -0
- package/src/orchestrator/templates/specs-checked-state.ts +31 -6
- package/src/orchestrator/templates/specs-mock.ts +12 -4
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
{{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
|
6
6
|
{{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
|
7
7
|
{{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
|
|
8
|
-
{{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
|
|
9
|
-
{{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
|
|
8
|
+
{{~#case 'id'}}locator('{{escapeQuotes (cssIdSelector locator value)}}'){{/case~}}
|
|
9
|
+
{{~#default}}locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}'){{/default~}}
|
|
10
10
|
{{~/switch}}{{else~}}
|
|
11
11
|
{{#if scope}}{{#if (eq scope "dialog")}}{{pageRoot}}.{{else}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{/if}}{{#switch strategy~}}
|
|
12
12
|
{{~#case 'testid'}}getByTestId('{{escapeQuotes value}}'){{/case~}}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
{{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
|
16
16
|
{{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
|
17
17
|
{{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
|
|
18
|
-
{{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
|
|
19
|
-
{{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
|
|
18
|
+
{{~#case 'id'}}locator('{{escapeQuotes (cssIdSelector locator value)}}'){{/case~}}
|
|
19
|
+
{{~#default}}locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}'){{/default~}}
|
|
20
20
|
{{~/switch}}{{else}}{{#switch strategy~}}
|
|
21
21
|
{{~#case 'testid'}}{{> locator-strategies/testid}}{{/case~}}
|
|
22
22
|
{{~#case 'role'}}{{> locator-strategies/role}}{{/case~}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
{{pageRoot}}.locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
{{pageRoot}}.locator('{{escapeQuotes (cssIdSelector locator value)}}')
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
{{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
|
6
6
|
{{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
|
7
7
|
{{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
|
|
8
|
-
{{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
|
|
9
|
-
{{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
|
|
8
|
+
{{~#case 'id'}}locator('{{escapeQuotes (cssIdSelector locator value)}}'){{/case~}}
|
|
9
|
+
{{~#default}}locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}'){{/default~}}
|
|
10
10
|
{{~/switch}}{{> locator-nth}}{{else~}}
|
|
11
11
|
{{#if scope}}{{#if (eq scope "dialog")}}{{pageRoot}}.{{else}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{/if}}{{#switch strategy~}}
|
|
12
12
|
{{~#case 'testid'}}getByTestId('{{escapeQuotes value}}'){{/case~}}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
{{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
|
16
16
|
{{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
|
17
17
|
{{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
|
|
18
|
-
{{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
|
|
19
|
-
{{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
|
|
18
|
+
{{~#case 'id'}}locator('{{escapeQuotes (cssIdSelector locator value)}}'){{/case~}}
|
|
19
|
+
{{~#default}}locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}'){{/default~}}
|
|
20
20
|
{{~/switch}}{{> locator-nth}}{{else}}{{#switch strategy~}}
|
|
21
21
|
{{~#case 'testid'}}{{> locator-strategies/testid}}{{/case~}}
|
|
22
22
|
{{~#case 'role'}}{{> locator-strategies/role}}{{/case~}}
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs
CHANGED
|
@@ -15,5 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
`expectPage`, not a bare `toHaveURL`: a polling URL assertion passes on the first transient
|
|
17
17
|
match, before a route guard has bounced the user away (#662). The helper arrives, lets the
|
|
18
|
-
page settle, holds, and re-asserts — read specs/url-assert.ts.
|
|
19
|
-
|
|
18
|
+
page settle, holds, and re-asserts — read specs/url-assert.ts.
|
|
19
|
+
|
|
20
|
+
`hostname` rides separately and only for an absolute page value, exactly as in the
|
|
21
|
+
`wait-for-page` twin: it is read off `u.hostname`, which never appears in `u.pathname`, so
|
|
22
|
+
one concatenated regex cannot carry both. Without it a page declared as
|
|
23
|
+
`https://admin.example.com/jobs` asserted the PATH on any host, and the public site's /jobs
|
|
24
|
+
satisfied it.
|
|
25
|
+
|
|
26
|
+
The declared-value half is gated on `dataRef`, not on `query`. The step wrote `with {{v}}`
|
|
27
|
+
either way; gating on the resolved VALUE meant an empty test-data entry silently dropped the
|
|
28
|
+
whole check and fell back to the bare-page rule, which is the one case specs/url-assert.ts
|
|
29
|
+
exists to throw on. --}}
|
|
30
|
+
await expectPage(page, { pathname: /^{{pathRegex}}\/?$/{{#if hostRegex}}, hostname: /^{{hostRegex}}$/{{/if}}{{#if dataRef}}, dataRef: '{{escapeQuotes dataRef}}', declared: '{{escapeQuotes query}}'{{/if}} });
|
|
@@ -49,6 +49,20 @@ test.describe('{{featureName}}', () => {
|
|
|
49
49
|
{{#each scenarios}}
|
|
50
50
|
{{indent this 2}}
|
|
51
51
|
|
|
52
|
+
{{/each}}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
{{else if noAuth}}
|
|
56
|
+
{{!-- @no-auth demands an EMPTY session (#735). Leaving the group bare inherits whatever
|
|
57
|
+
playwright.config.ts set through `use.storageState`, so "an anonymous visitor is redirected to
|
|
58
|
+
sign-in" ran signed in and passed for the wrong reason. Omitting storageState inherits it —
|
|
59
|
+
an inline empty state is the only way to say "no session". --}}
|
|
60
|
+
test.describe('no-auth', () => {
|
|
61
|
+
test.use({ storageState: { cookies: [], origins: [] } });
|
|
62
|
+
|
|
63
|
+
{{#each scenarios}}
|
|
64
|
+
{{indent this 2}}
|
|
65
|
+
|
|
52
66
|
{{/each}}
|
|
53
67
|
});
|
|
54
68
|
|
|
@@ -167,23 +167,51 @@ function extractAuthRole(tags: string[]): string | undefined {
|
|
|
167
167
|
* Precedence: Scenario > Feature > None
|
|
168
168
|
*/
|
|
169
169
|
function getEffectiveAuthRole(
|
|
170
|
-
scenarioTags: string[],
|
|
170
|
+
scenarioTags: string[],
|
|
171
171
|
featureTags: string[]
|
|
172
172
|
): string | undefined {
|
|
173
173
|
// Check for explicit @no-auth override
|
|
174
174
|
if (scenarioTags.some(tag => tag === '@no-auth')) {
|
|
175
175
|
return undefined;
|
|
176
176
|
}
|
|
177
|
-
|
|
177
|
+
|
|
178
178
|
// Scenario-level takes precedence
|
|
179
179
|
const scenarioRole = extractAuthRole(scenarioTags);
|
|
180
180
|
if (scenarioRole) return scenarioRole;
|
|
181
|
-
|
|
181
|
+
|
|
182
182
|
// Fall back to feature-level
|
|
183
183
|
const featureRole = extractAuthRole(featureTags);
|
|
184
184
|
return featureRole;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* The three auth states a scenario can be in — they are NOT two (#735).
|
|
189
|
+
*
|
|
190
|
+
* `@no-auth` and "no auth tag at all" both leave `getEffectiveAuthRole` undefined, but they mean
|
|
191
|
+
* opposite things: the first demands an EMPTY session, the second inherits whatever
|
|
192
|
+
* `playwright.config.ts` set via `use.storageState`. Collapsing them put both in one group, so a
|
|
193
|
+
* scenario written to prove "an anonymous visitor is bounced to sign-in" ran signed in and passed
|
|
194
|
+
* for the wrong reason — while the exported test case still printed "Not authenticated".
|
|
195
|
+
*
|
|
196
|
+
* Returned as a flag beside the role rather than a tagged union so every existing `authRole` reader
|
|
197
|
+
* keeps working unchanged; only the grouping and the template consult `noAuth`.
|
|
198
|
+
*/
|
|
199
|
+
function getAuthMode(
|
|
200
|
+
scenarioTags: string[],
|
|
201
|
+
featureTags: string[],
|
|
202
|
+
): { noAuth: boolean; authRole: string | undefined } {
|
|
203
|
+
if (scenarioTags.some(tag => tag === '@no-auth')) {
|
|
204
|
+
return { noAuth: true, authRole: undefined };
|
|
205
|
+
}
|
|
206
|
+
return { noAuth: false, authRole: getEffectiveAuthRole(scenarioTags, featureTags) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Grouping key for the anonymous bucket. A literal that no `@auth:<role>` can produce, so an
|
|
211
|
+
* imaginative role name can never land in the same group as `@no-auth`.
|
|
212
|
+
*/
|
|
213
|
+
const NO_AUTH_GROUP_KEY = 'no-auth';
|
|
214
|
+
|
|
187
215
|
|
|
188
216
|
/**
|
|
189
217
|
* True when the scenario has at least one step in the Then bucket —
|
|
@@ -699,7 +727,7 @@ export class CodeGenerator {
|
|
|
699
727
|
// absent compiles to a skip stub instead of a real test (read once per feature).
|
|
700
728
|
const enabledCaps = new Set<string>(readCapabilities(process.cwd()).enabled.map(d => d.toLowerCase()));
|
|
701
729
|
|
|
702
|
-
const renderedScenarios: Array<{ code: string; authRole?: string }> = [];
|
|
730
|
+
const renderedScenarios: Array<{ code: string; authRole?: string; noAuth?: boolean }> = [];
|
|
703
731
|
for (const scenario of feature.scenarios) {
|
|
704
732
|
if (isManual(scenario.tags)) {
|
|
705
733
|
// SG-W008 — two-direction rule for @manual scenarios:
|
|
@@ -758,7 +786,14 @@ export class CodeGenerator {
|
|
|
758
786
|
const reqAbsent = requiresCaps(scenario.tags).filter(c => !enabledCaps.has(c));
|
|
759
787
|
if (reqAbsent.length) {
|
|
760
788
|
if (this.options.verbose) console.log(` ⏸ Pending capability (${reqAbsent.join(', ')}): ${scenario.name}`);
|
|
761
|
-
|
|
789
|
+
// Carries its `@no-auth` tag like any other scenario so the stub lands in the group the
|
|
790
|
+
// author declared — a skipped test that reappears once the capability is enabled must not
|
|
791
|
+
// quietly change auth bucket.
|
|
792
|
+
renderedScenarios.push({
|
|
793
|
+
code: this.generateRequiresSkipStub(scenario.name, reqAbsent),
|
|
794
|
+
authRole: undefined,
|
|
795
|
+
noAuth: scenario.tags.includes('@no-auth'),
|
|
796
|
+
});
|
|
762
797
|
continue;
|
|
763
798
|
}
|
|
764
799
|
|
|
@@ -770,7 +805,7 @@ export class CodeGenerator {
|
|
|
770
805
|
authFeatureTags = [...baseScenario.tags, ...authFeatureTags];
|
|
771
806
|
}
|
|
772
807
|
}
|
|
773
|
-
const authRole =
|
|
808
|
+
const { authRole, noAuth } = getAuthMode(scenario.tags, authFeatureTags);
|
|
774
809
|
|
|
775
810
|
const code = await this.generateScenario(
|
|
776
811
|
scenario,
|
|
@@ -778,15 +813,16 @@ export class CodeGenerator {
|
|
|
778
813
|
feature.tags || [],
|
|
779
814
|
isParallel
|
|
780
815
|
);
|
|
781
|
-
renderedScenarios.push({ code, authRole });
|
|
816
|
+
renderedScenarios.push({ code, authRole, noAuth });
|
|
782
817
|
}
|
|
783
818
|
|
|
784
819
|
// Group scenarios by auth role for nested test.describe blocks
|
|
785
|
-
// This ensures test.use({ storageState }) only applies to its group
|
|
820
|
+
// This ensures test.use({ storageState }) only applies to its group. `@no-auth` gets its OWN
|
|
821
|
+
// bucket (#735) — it is a demand for an empty session, not the absence of a demand.
|
|
786
822
|
const authGroupMap = new Map<string, string[]>();
|
|
787
823
|
const groupOrder: string[] = [];
|
|
788
|
-
for (const { code, authRole } of renderedScenarios) {
|
|
789
|
-
const key = authRole || '';
|
|
824
|
+
for (const { code, authRole, noAuth } of renderedScenarios) {
|
|
825
|
+
const key = noAuth ? NO_AUTH_GROUP_KEY : (authRole || '');
|
|
790
826
|
if (!authGroupMap.has(key)) {
|
|
791
827
|
authGroupMap.set(key, []);
|
|
792
828
|
groupOrder.push(key);
|
|
@@ -795,7 +831,8 @@ export class CodeGenerator {
|
|
|
795
831
|
}
|
|
796
832
|
|
|
797
833
|
const authGroups = groupOrder.map(key => ({
|
|
798
|
-
authRole: key || undefined,
|
|
834
|
+
authRole: key === NO_AUTH_GROUP_KEY ? undefined : (key || undefined),
|
|
835
|
+
noAuth: key === NO_AUTH_GROUP_KEY,
|
|
799
836
|
scenarios: authGroupMap.get(key)!,
|
|
800
837
|
}));
|
|
801
838
|
|
|
@@ -804,6 +841,15 @@ export class CodeGenerator {
|
|
|
804
841
|
// - Multiple groups: nested describes per auth role
|
|
805
842
|
const needsGrouping = authGroups.length > 1;
|
|
806
843
|
|
|
844
|
+
// Rendering the per-group blocks is a SEPARATE question from the guards below (#735). A lone
|
|
845
|
+
// `@no-auth` group is one group — `needsGrouping` is false — yet it still has to emit its empty
|
|
846
|
+
// storageState, or an all-anonymous feature silently runs on the config's session. Keeping the
|
|
847
|
+
// guards keyed on `needsGrouping` means this never trips the appium/serial errors, which are
|
|
848
|
+
// about MIXING roles. Serial is excluded because its layout builds the context by hand with
|
|
849
|
+
// `browser.newContext()`, which never picks up `use.storageState` — already anonymous, and a
|
|
850
|
+
// `test.use` there would apply to nothing.
|
|
851
|
+
const renderAuthBlocks = needsGrouping || (isParallel && authGroups.some(g => g.noAuth));
|
|
852
|
+
|
|
807
853
|
// D6 — the Appium test-file template has no per-group before-hook (only `singleAuthRole` runs
|
|
808
854
|
// `__ensureAuth`); >1 auth group would render every group's scenarios with NO authentication at
|
|
809
855
|
// all, silently. Mirrors the web `@parallel` guard below, but unconditional (isParallel has no
|
|
@@ -819,15 +865,18 @@ export class CodeGenerator {
|
|
|
819
865
|
|
|
820
866
|
if (needsGrouping && !isParallel) {
|
|
821
867
|
throw new Error(
|
|
822
|
-
`Feature "${feature.name}" has multiple auth groups but
|
|
868
|
+
`Feature "${feature.name}" has multiple auth groups but runs serially.\n` +
|
|
823
869
|
`Serial mode uses a shared browser context — it cannot mix different auth roles.\n` +
|
|
824
|
-
`Fix:
|
|
870
|
+
`Fix: remove the @serial tag (features are parallel by default), or split "${feature.name}" ` +
|
|
871
|
+
`into one feature file per auth role.`
|
|
825
872
|
);
|
|
826
873
|
}
|
|
827
874
|
const scenarios = renderedScenarios.map(s => s.code);
|
|
828
875
|
|
|
829
|
-
// For single group, extract the auth role to put test.use at describe level
|
|
830
|
-
|
|
876
|
+
// For single group, extract the auth role to put test.use at describe level. Keyed on
|
|
877
|
+
// `renderAuthBlocks`, not `needsGrouping`: a lone `@no-auth` group now renders its own block, so
|
|
878
|
+
// hoisting anything to the feature describe would double up. Its authRole is undefined anyway.
|
|
879
|
+
const singleAuthRole = !renderAuthBlocks && authGroups.length === 1
|
|
831
880
|
? authGroups[0].authRole
|
|
832
881
|
: undefined;
|
|
833
882
|
|
|
@@ -932,8 +981,8 @@ export class CodeGenerator {
|
|
|
932
981
|
platform,
|
|
933
982
|
cleanup,
|
|
934
983
|
backgroundSteps,
|
|
935
|
-
scenarios:
|
|
936
|
-
authGroups:
|
|
984
|
+
scenarios: renderAuthBlocks ? [] : scenarios,
|
|
985
|
+
authGroups: renderAuthBlocks ? authGroups : undefined,
|
|
937
986
|
singleAuthRole,
|
|
938
987
|
appPackage,
|
|
939
988
|
iosBundleId,
|
|
@@ -218,9 +218,46 @@
|
|
|
218
218
|
* signal (SG-W030) it is not dropped-and-warned: with no signal the step is back on the
|
|
219
219
|
* failure the declaration exists to fix. Recorded here AND thrown, so a pattern that
|
|
220
220
|
* resolves selectors inside a `catch` cannot swallow it silently
|
|
221
|
+
* SG-W032 — a step carries the row filter `in [T] table with {{v}}` but the pattern that won it
|
|
222
|
+
* dropped the clause, so the generated code runs against the WHOLE table. Reproduced
|
|
223
|
+
* live: `Then User see [Status] text in [Job Table] table with {{v}}` asserts on every
|
|
224
|
+
* `.status` in the table and passes on a row the sentence never named. Judged on the
|
|
225
|
+
* emitted code — row scoping compiles to `.filter(` on every path that honours it — so
|
|
226
|
+
* the `table-*` family (which resolves the container itself, positioned rows included)
|
|
227
|
+
* and steps already refused elsewhere are excluded, as is the two-`with` form, which
|
|
228
|
+
* binds its tail to the parent and is the rewrite this message points at. A step that
|
|
229
|
+
* WRITES is excluded too: a write needs a value, so its `with {{v}}` IS that value and
|
|
230
|
+
* cannot also be a row filter — `fill [Search] field in [T] table with {{v}}` is an
|
|
231
|
+
* ordinary container-scoped write
|
|
232
|
+
* SG-W034 — a `see [X] column with {{v}}` step runs inside a table whose rows are declared with a
|
|
233
|
+
* `rows:` SHAPE, so the row is not an ARIA row and holds no ARIA cells — yet the
|
|
234
|
+
* assertion's only other way to reach a cell is `getByRole('cell')`, which matches
|
|
235
|
+
* nothing there. `columns.<col>.index` cannot rescue it either: it indexes that same
|
|
236
|
+
* empty set. Silent before this, and silent in the worst way — `toBeVisible()` on an
|
|
237
|
+
* empty locator fails as a timeout blaming the app. The message names the
|
|
238
|
+
* `columns.<col>.elements.cell` declaration that resolves the cell directly. Scoped to
|
|
239
|
+
* the `rows:` case on purpose: on a real <table> the role fallback is correct, and a
|
|
240
|
+
* diagnostic that fires on the correct spelling gets muted along with the real ones
|
|
241
|
+
* SG-E035 — `table match data:` on a table whose rows are declared with a `rows:` SHAPE and whose
|
|
242
|
+
* columns are not declared (or do not cover every column of the DataTable). The
|
|
243
|
+
* assertion reads the header row from `thead tr` and addresses cells as `td, th`;
|
|
244
|
+
* a list built from <div>s has neither, forever — not a render race — so the step can
|
|
245
|
+
* only wait out every timeout and fail on an empty header row. Refused at compile time
|
|
246
|
+
* instead, naming the `columns:` entries it needs. Declaring them replaces the runtime
|
|
247
|
+
* header read entirely: the index is known at compile time, and a column that names
|
|
248
|
+
* its own `elements.cell` addresses the cell without any table role at all
|
|
249
|
+
* SG-W036 — a `remember [X] … as {{v}}` capture whose entry selects by SHAPE (`locator`/`css`)
|
|
250
|
+
* and pins no `nth:`. A capture reads ONE element and the template takes the first
|
|
251
|
+
* match — the right default, and exactly what the documented `nth: 0` workaround asked
|
|
252
|
+
* for after Playwright's strict mode turned a list-shaped selector into a hard failure
|
|
253
|
+
* naming the count instead of the step. But "the first of many" is a decision: if the
|
|
254
|
+
* selector describes every item in a list, the captured value is the first row's rather
|
|
255
|
+
* than the one the scenario means, and nothing else would ever say so. Not reported for
|
|
256
|
+
* testid/label/placeholder/role+name/text entries, which name a particular thing —
|
|
257
|
+
* warning there would fire on nearly every capture in a suite
|
|
221
258
|
* Diagnostics never change the EXIT CODE by themselves; `sungen generate --strict` turns any
|
|
222
259
|
* collected diagnostic into a non-zero exit. They are not all inert, though: SG-W001, SG-W011,
|
|
223
|
-
* SG-E020, SG-E021, SG-W020, SG-E023, SG-E024, SG-E025, SG-E026 and SG-
|
|
260
|
+
* SG-E020, SG-E021, SG-W020, SG-E023, SG-E024, SG-E025, SG-E026, SG-E027 and SG-E035 additionally compile
|
|
224
261
|
* the offending step to a runtime `throw` — a step that reads as a check and silently checks
|
|
225
262
|
* nothing is worse than one that fails loudly — and SG-E031 refuses the file outright. (SG-E022 is
|
|
226
263
|
* the one exception to the "blame the Gherkin" rule and throws nothing: the file is still written
|
|
@@ -228,7 +265,7 @@
|
|
|
228
265
|
* Generation always COMPLETES — the rest of the scenario and the rest of the file still compile.
|
|
229
266
|
*/
|
|
230
267
|
|
|
231
|
-
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W004' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014' | 'SG-W015' | 'SG-W016' | 'SG-W017' | 'SG-W018' | 'SG-E020' | 'SG-E021' | 'SG-W020' | 'SG-W021' | 'SG-W022' | 'SG-W023' | 'SG-W024' | 'SG-W026' | 'SG-E025' | 'SG-E026' | 'SG-E022' | 'SG-E023' | 'SG-E024' | 'SG-E027' | 'SG-W028' | 'SG-W029' | 'SG-W030' | 'SG-E031';
|
|
268
|
+
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W004' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014' | 'SG-W015' | 'SG-W016' | 'SG-W017' | 'SG-W018' | 'SG-E020' | 'SG-E021' | 'SG-W020' | 'SG-W021' | 'SG-W022' | 'SG-W023' | 'SG-W024' | 'SG-W026' | 'SG-E025' | 'SG-E026' | 'SG-E022' | 'SG-E023' | 'SG-E024' | 'SG-E027' | 'SG-W028' | 'SG-W029' | 'SG-W030' | 'SG-E031' | 'SG-W032' | 'SG-W034' | 'SG-E035' | 'SG-W036';
|
|
232
269
|
|
|
233
270
|
export interface Diagnostic {
|
|
234
271
|
code: DiagnosticCode;
|
|
@@ -48,7 +48,7 @@ const TAP_GESTURE_FORMS = /\bdouble[-\s]?tap|\btap(?:s)?\b[^.]*\b(?:top of|at th
|
|
|
48
48
|
* one of the two mobile patterns that must stay UNDECLARED.
|
|
49
49
|
* 2. Both vocabularies then see their own verb, and ordinary PRIORITY decides the winner —
|
|
50
50
|
* no matcher has to learn about the other. `tap … and accept alert` reaches
|
|
51
|
-
* `click-with-alert` (21); `tap … in [T] table row 2` reaches `table-action-in-row-nth` (
|
|
51
|
+
* `click-with-alert` (21); `tap … in [T] table row 2` reaches `table-action-in-row-nth` (18);
|
|
52
52
|
* a plain `tap` still lands on `mobile-tap` (15) ahead of `click-element` (10).
|
|
53
53
|
*
|
|
54
54
|
* Safe because every click-keyed matcher in the vocabulary is a word-boundary test (`\bclick(s)?\b`
|
|
@@ -3,7 +3,7 @@ import { PARENT_CONTAINER_TYPES } from '../gherkin-parser/parent-scope-clause';
|
|
|
3
3
|
import { TemplateEngine } from './template-engine';
|
|
4
4
|
import { adapterRegistry } from './adapters';
|
|
5
5
|
import { PatternRegistry, PatternContext, Platform } from './patterns';
|
|
6
|
-
import { SelectorResolver } from './utils/selector-resolver';
|
|
6
|
+
import { SelectorResolver, type ResolvedSelector } from './utils/selector-resolver';
|
|
7
7
|
import { DataResolver } from './utils/data-resolver';
|
|
8
8
|
import { locatorIdentityKey } from './utils/dialog-scope-identity';
|
|
9
9
|
import { DiagnosticsCollector } from './diagnostics';
|
|
@@ -470,6 +470,8 @@ export class StepMapper {
|
|
|
470
470
|
if (positionDropped) return positionDropped;
|
|
471
471
|
const swallowed = this.checkAssertionClauseSwallowed(step, mappedStep);
|
|
472
472
|
if (swallowed) return swallowed;
|
|
473
|
+
const rowFilterDropped = this.checkRowFilterClauseDropped(step, mappedStep);
|
|
474
|
+
if (rowFilterDropped) return rowFilterDropped;
|
|
473
475
|
const badType = this.checkUnknownSelectorType(step, mappedStep);
|
|
474
476
|
if (badType) return badType;
|
|
475
477
|
return mappedStep;
|
|
@@ -680,6 +682,69 @@ export class StepMapper {
|
|
|
680
682
|
};
|
|
681
683
|
}
|
|
682
684
|
|
|
685
|
+
/**
|
|
686
|
+
* The `in [T] table with {{v}}` row filter is the sentence's narrowest claim — it names ONE row.
|
|
687
|
+
* A pattern that does not consume it leaves the step running against the WHOLE table, and the
|
|
688
|
+
* author cannot see that from the Gherkin.
|
|
689
|
+
*
|
|
690
|
+
* Judged on the emitted code, not on which pattern won: `.filter(` is what row scoping compiles
|
|
691
|
+
* to on every path that honours the clause, so its absence is the fact. Three exclusions, each
|
|
692
|
+
* measured rather than assumed:
|
|
693
|
+
* - the `table-*` family resolves the container from the step text itself; its positioned-row
|
|
694
|
+
* form emits `toHaveCount(1)` + `toContainText` with no filter at all and is CORRECT;
|
|
695
|
+
* - a step already refused elsewhere compiles to a `throw`, and a second opinion on top of the
|
|
696
|
+
* refusal (an off-platform table step already gets SG-W020) is two contradictory messages
|
|
697
|
+
* about one step;
|
|
698
|
+
* - the two-`with` form binds its tail to the PARENT, which does compile to a filter — it is
|
|
699
|
+
* the shape this diagnostic points people at, so it must never be flagged.
|
|
700
|
+
*
|
|
701
|
+
* A step that WRITES is exempt rather than escalated — see ROW_FILTER_WRITE_CALL. It stays a
|
|
702
|
+
* WARNING for the rest, in line with the repo's rule that a diagnostic which cries wolf gets muted
|
|
703
|
+
* wholesale: the single-`with` sentence is genuinely ambiguous, and reading the value as the
|
|
704
|
+
* expected text is a legitimate compilation of it, just not the one most authors mean.
|
|
705
|
+
*/
|
|
706
|
+
private static readonly ROW_FILTER_CLAUSE = /\bin\s+\[[^\]]+\]\s+table\s+with\b/i;
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Calls that WRITE through the scoped locator, as opposed to reading it. Both adapters are listed:
|
|
710
|
+
* the mobile spelling of a fill is `setValue`/`addValue`.
|
|
711
|
+
*
|
|
712
|
+
* A write is EXEMPT, not escalated. A step that writes needs a value, so its `with {{v}}` is that
|
|
713
|
+
* value and cannot also be a row filter — `fill [Search] field in [Job Table] table with {{v}}` is
|
|
714
|
+
* an ordinary container-scoped write, and a toolbar search box inside a table is exactly that. The
|
|
715
|
+
* danger an earlier reading claimed for it does not exist either: on a multi-row table a
|
|
716
|
+
* mis-scoped write is a loud strict-mode failure, and on a single-row table there is only one
|
|
717
|
+
* field to write to.
|
|
718
|
+
*/
|
|
719
|
+
private static readonly ROW_FILTER_WRITE_CALL =
|
|
720
|
+
/\.(?:fill|type|setInputFiles|selectOption|clear|check|uncheck|press|setValue|addValue|clearValue)\(/;
|
|
721
|
+
|
|
722
|
+
private checkRowFilterClauseDropped(step: ParsedStep, mapped: MappedStep): MappedStep | null {
|
|
723
|
+
const code = mapped.code || '';
|
|
724
|
+
if (!StepMapper.ROW_FILTER_CLAUSE.test(step.text)) return null;
|
|
725
|
+
// Bound to the parent instead of the target — that IS the scoped form.
|
|
726
|
+
if (step.parentValueRef || step.parentValueText) return null;
|
|
727
|
+
if (mapped.pattern?.startsWith('table-')) return null;
|
|
728
|
+
if (code.startsWith('throw new Error(')) return null;
|
|
729
|
+
if (/\.filter\(/.test(code)) return null;
|
|
730
|
+
|
|
731
|
+
if (StepMapper.ROW_FILTER_WRITE_CALL.test(code)) return null;
|
|
732
|
+
|
|
733
|
+
const table = step.parentRef ?? 'Table';
|
|
734
|
+
const target = step.selectorRef ?? 'X';
|
|
735
|
+
const type = step.elementType ?? 'text';
|
|
736
|
+
const filter = step.dataRef ? `{{${step.dataRef}}}` : 'the row value';
|
|
737
|
+
const rewrite = `User see [${target}] ${type} with <expected> in [<Row Entry>] row with ${filter}`;
|
|
738
|
+
this.diagnostics?.add('SG-W032',
|
|
739
|
+
`step "${step.text}" reads "with ${filter}" as the EXPECTED VALUE, not as a row filter — the `
|
|
740
|
+
+ `sentence names a row but the assertion runs against the whole [${table}] table, so it can `
|
|
741
|
+
+ `pass on a row the sentence never named. To scope the row, use the two-"with" form: `
|
|
742
|
+
+ `"${rewrite}". Point that second \`[…]\` at a ROW entry, not at the table entry — a filter `
|
|
743
|
+
+ `applied to the table narrows nothing and the assertion stays as weak as it is now.`,
|
|
744
|
+
{ feature: this.featureName, step: step.text });
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
|
|
683
748
|
private checkAssertionClauseSwallowed(step: ParsedStep, mapped: MappedStep): MappedStep | null {
|
|
684
749
|
const code = mapped.code || '';
|
|
685
750
|
// Bare visibility: nothing but a visible/displayed assertion on one locator.
|
|
@@ -804,21 +869,57 @@ export class StepMapper {
|
|
|
804
869
|
|
|
805
870
|
// Try to find column index from table's columns config
|
|
806
871
|
let columnIndex: number | undefined;
|
|
872
|
+
// The CELL, when the table declares one. `getByRole('cell')` below is the only other way this
|
|
873
|
+
// template can reach a cell, and a row shape built from <div>s has no cell role anywhere in it
|
|
874
|
+
// — the assertion then matches zero elements on a list that renders perfectly well. A
|
|
875
|
+
// `columns.<col>.elements.cell` entry names the cell directly and sidesteps the role entirely.
|
|
876
|
+
// `columns.<col>.index` cannot help there: it indexes the very getByRole('cell') set that is
|
|
877
|
+
// empty.
|
|
878
|
+
let cellLocator: string | undefined;
|
|
807
879
|
if (this.rowScopeTable) {
|
|
880
|
+
// Only the table LOOKUP is guarded. Widening the catch to cover the cell resolution below
|
|
881
|
+
// would swallow a broken `elements.cell` declaration under a comment about a missing table,
|
|
882
|
+
// and silently serve the ARIA fallback with no diagnostic at all.
|
|
883
|
+
let tableResolved: ResolvedSelector | undefined;
|
|
808
884
|
try {
|
|
809
|
-
|
|
885
|
+
tableResolved = this.selectorResolver.resolveSelector(
|
|
810
886
|
this.rowScopeTable, this.featureName, 'table', 0
|
|
811
887
|
);
|
|
812
|
-
const columnKey = columnRef.toLowerCase().normalize('NFC');
|
|
813
|
-
columnIndex = tableResolved.columns?.[columnKey]?.index;
|
|
814
888
|
} catch {
|
|
815
889
|
// Table not found — fallback to filter
|
|
816
890
|
}
|
|
891
|
+
if (tableResolved) {
|
|
892
|
+
const columnKey = columnRef.toLowerCase().normalize('NFC');
|
|
893
|
+
const column = tableResolved.columns?.[columnKey];
|
|
894
|
+
columnIndex = column?.index;
|
|
895
|
+
const cellEntry = column?.elements?.cell;
|
|
896
|
+
// A shape that still selects <tr> keeps its ARIA rows and cells, so the role fallback below
|
|
897
|
+
// serves it exactly as before. Only a shape built from something else loses them.
|
|
898
|
+
const shapeHidesTableRoles = !!tableResolved.rows
|
|
899
|
+
&& !/(^|[\s>+~,(])tr\b/i.test(tableResolved.rows);
|
|
900
|
+
if (cellEntry) {
|
|
901
|
+
cellLocator = this.templateEngine.renderStep('locator', {
|
|
902
|
+
...this.selectorResolver.resolveNestedEntry(cellEntry, columnRef),
|
|
903
|
+
parentLocator: 'tableRow',
|
|
904
|
+
});
|
|
905
|
+
} else if (shapeHidesTableRoles) {
|
|
906
|
+
// The rows are not ARIA rows, so their children are not ARIA cells either. Warned only in
|
|
907
|
+
// that case: on a real <table> — including one that pins its row universe with
|
|
908
|
+
// `rows: 'tbody tr'` — the role fallback is correct, and a diagnostic that fires on the
|
|
909
|
+
// correct spelling gets muted along with the real ones.
|
|
910
|
+
this.diagnostics?.add('SG-W034',
|
|
911
|
+
`column "[${columnRef}]" is asserted inside a table whose rows are declared with a "rows:" `
|
|
912
|
+
+ `shape, so the row is not an ARIA row and holds no ARIA cells — this step falls back to `
|
|
913
|
+
+ `getByRole('cell'), which matches nothing there. Name the cell on the column, e.g. `
|
|
914
|
+
+ `"${this.rowScopeTable.toLowerCase()}:\\n columns:\\n ${columnKey}:\\n index: 0\\n header: '${columnRef}'\\n elements:\\n cell:\\n type: locator\\n value: '.your-cell-class'"`,
|
|
915
|
+
{ feature: this.featureName, step: step.text });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
817
918
|
}
|
|
818
919
|
|
|
819
920
|
const isGiven = this.lastPrimaryKeyword === 'Given';
|
|
820
921
|
const code = this.templateEngine.renderStep('row-scoped-column-assertion', {
|
|
821
|
-
columnIndex, cellValue, isGiven,
|
|
922
|
+
columnIndex, cellValue, isGiven, cellLocator,
|
|
822
923
|
});
|
|
823
924
|
|
|
824
925
|
return {
|
|
@@ -53,6 +53,24 @@ export class TemplateEngine {
|
|
|
53
53
|
.replace(/\u2029/g, '\\u2029');
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
// The CSS selector behind an `id` strategy, from whichever field carries it.
|
|
57
|
+
//
|
|
58
|
+
// Two shapes reach this with two different conventions. A new-format entry (`type: id`) keeps
|
|
59
|
+
// whatever the author wrote in `value`; the legacy `resolveByPriority` path STRIPS the hash,
|
|
60
|
+
// returning `value: 'submit-btn'` with the full `#submit-btn` in `locator`. Reading `value`
|
|
61
|
+
// alone would therefore compile `#submit-btn` down to `page.locator('submit-btn')` — a type
|
|
62
|
+
// selector that matches nothing, which an `is hidden` assertion then passes forever.
|
|
63
|
+
//
|
|
64
|
+
// A bare word gets its `#` back for the same reason: `id` is also a mobile strategy, where the
|
|
65
|
+
// convention IS bare ("bottom_bar"), so an entry copied across platforms would otherwise be a
|
|
66
|
+
// silent no-match on web. Anything already carrying selector syntax is passed through untouched.
|
|
67
|
+
Handlebars.registerHelper('cssIdSelector', function(locator: any, value: any) {
|
|
68
|
+
const raw = (typeof locator === 'string' && locator) ? locator : (value ?? '');
|
|
69
|
+
const text = String(raw);
|
|
70
|
+
if (!text) return '';
|
|
71
|
+
return /[#.\[\]:>~+ ]/.test(text) ? text : `#${text}`;
|
|
72
|
+
});
|
|
73
|
+
|
|
56
74
|
Handlebars.registerHelper('includes', function(text: string, search: string) {
|
|
57
75
|
return typeof text === 'string' && text.includes(search);
|
|
58
76
|
});
|
|
@@ -751,6 +751,15 @@ export class SelectorResolver {
|
|
|
751
751
|
return out.class || out.attribute ? out : undefined;
|
|
752
752
|
}
|
|
753
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Resolve a selectors.yaml entry that is NOT reachable by key — a `columns.<col>.elements.cell`
|
|
756
|
+
* declaration, which lives nested under a table entry. Public for that one caller; everything
|
|
757
|
+
* addressable by name goes through `resolveSelector`.
|
|
758
|
+
*/
|
|
759
|
+
resolveNestedEntry(entry: SelectorEntry, originalLabel: string): ResolvedSelector {
|
|
760
|
+
return this.resolveFromEntry(entry, originalLabel);
|
|
761
|
+
}
|
|
762
|
+
|
|
754
763
|
private resolveFromEntry(entry: SelectorEntry, originalLabel: string): ResolvedSelector {
|
|
755
764
|
// Default values
|
|
756
765
|
const locator = entry.locator || '';
|
|
@@ -971,9 +980,15 @@ export class SelectorResolver {
|
|
|
971
980
|
exact,
|
|
972
981
|
});
|
|
973
982
|
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
//
|
|
983
|
+
// Native strategies — pass through unchanged; the appium adapter's `appium-selector` switch
|
|
984
|
+
// renders the right WDIO selector (`~` / raw xpath / `android=` / `-ios predicate string:` /
|
|
985
|
+
// `id=`). See issue #392.
|
|
986
|
+
//
|
|
987
|
+
// `id` is NOT mobile-only, whatever this comment used to claim: the legacy branch at the
|
|
988
|
+
// bottom of resolveByPriority extracts `#id` out of a web `locator:` and returns
|
|
989
|
+
// `strategy: 'id'`, and the playwright adapter ships an `id` partial to serve it. The two
|
|
990
|
+
// paths disagree on whether `value` carries the hash, which is why the partial reads the
|
|
991
|
+
// selector through `cssIdSelector` rather than off `value` directly.
|
|
977
992
|
case 'accessibility-id':
|
|
978
993
|
case 'xpath':
|
|
979
994
|
case 'android-uiautomator':
|
package/src/harness/audit.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { specCoverage, SpecCoverageResult, parseSpecClauses, restatedRequirement
|
|
|
27
27
|
import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
|
|
28
28
|
negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk, serialCascadeRisk } from './quality-gates';
|
|
29
29
|
import { viewpointLedger, parseViewpointItems, browserGestureSubstitutions, substitutionRemedy, viewpointIntegrity, LedgerResult } from './viewpoint-ledger';
|
|
30
|
-
import { specBranches, unenumeratedBranches, sameOracleClusters,
|
|
30
|
+
import { specBranches, unenumeratedBranches, sameOracleClusters, visibilityPairGaps, awaitingSeedData, unconsideredRisks, MULTI_SCREEN_RISKS } from './spec-branches';
|
|
31
31
|
const MULTI_SCREEN_RISK_COUNT = MULTI_SCREEN_RISKS.length;
|
|
32
32
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
33
33
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
@@ -243,7 +243,18 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
243
243
|
const downstream = downstreamScope(readText(specPath), scenarios, featureText);
|
|
244
244
|
const manualOracleResult = manualOracle(featureText);
|
|
245
245
|
const autoManual = automatableManual(scenarios); // TQ-2 — @manual that is really automatable
|
|
246
|
-
|
|
246
|
+
// The ledger decides "covered" by looking the claim's id up in the feature TEXT, and a comment
|
|
247
|
+
// is text: a file whose header comment merely mentions VP-74 scored it covered while no scenario
|
|
248
|
+
// implemented it. That is a false GREEN on a critical axis — the one kind of wrong answer an
|
|
249
|
+
// audit must not give. Comments live on their own lines in Gherkin, so dropping those lines is
|
|
250
|
+
// the whole fix; no parser is needed.
|
|
251
|
+
//
|
|
252
|
+
// Stripped HERE and nowhere else. The shared `featureText` above feeds twelve other readers, one
|
|
253
|
+
// of which (`manualOracle`) reads `#` lines ON PURPOSE — a `@manual` scenario with no steps
|
|
254
|
+
// carries its entire procedure in comments — so stripping at the source would delete that
|
|
255
|
+
// oracle's only input.
|
|
256
|
+
const ledgerFeatureText = featureText.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
|
|
257
|
+
const ledger = viewpointLedger(viewpointPath, scenarios, ledgerFeatureText);
|
|
247
258
|
const negSideEffect = negativeSideEffect(scenarios);
|
|
248
259
|
const ownership = crossArtifactOwnership(screenDir, scenarios);
|
|
249
260
|
// @exploration scenarios are probes outside the official suite — the finding's own fix
|
|
@@ -646,6 +657,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
646
657
|
for (const u of spec.uncoveredMust) {
|
|
647
658
|
findings.push(`SPEC-UNCOVERED: ${u.id} (MUST) has no covering scenario — "${u.text}" → add a scenario or tag one @spec:${u.id}.`);
|
|
648
659
|
}
|
|
660
|
+
// #679 — a requirement that states both directions is two requirements wearing one id. Citing
|
|
661
|
+
// the id closes the id, not the clause the suite never asserts.
|
|
662
|
+
for (const c of spec.clauseGaps) {
|
|
663
|
+
findings.push(`SPEC-CLAUSE-UNCOVERED: ${c.id} states both directions and only one is proved — nothing asserts its ${c.polarity === 'negate' ? 'MUST NOT' : 'MUST'} clause ("${c.text}"). One scenario tagged @spec:${c.id} cannot demonstrate a rule and its opposite: the states are mutually exclusive, so each needs its own case. Write the ${c.polarity === 'negate' ? 'negative' : 'positive'} case, or narrow the requirement to the direction the suite actually covers.`);
|
|
664
|
+
}
|
|
649
665
|
if (spec.inferredOnly.length > 0) {
|
|
650
666
|
// Covered, but only by keyword inference — no scenario cites the id. Delivery's
|
|
651
667
|
// requirement table (and any later refactor) cannot follow an inferred link, so
|
|
@@ -742,9 +758,12 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
742
758
|
}
|
|
743
759
|
}
|
|
744
760
|
// #630 — a deny-side security case with no allow-side counterpart cannot tell a working guard
|
|
745
|
-
// from a feature that is broken for everyone.
|
|
746
|
-
|
|
747
|
-
|
|
761
|
+
// from a feature that is broken for everyone. #679 — the same reasoning holds for any condition,
|
|
762
|
+
// so the trigger is the ELEMENT proved absent and never proved present, not permission wording.
|
|
763
|
+
for (const g of visibilityPairGaps(scenarios).slice(0, 4)) {
|
|
764
|
+
findings.push(g.kind === 'permission'
|
|
765
|
+
? `PERMISSION-PAIR-MISSING: "${g.scenario}" proves ${g.element ? `[${g.element}]` : 'the content'} is hidden when permission is denied, but no scenario proves it is SHOWN when permission is granted. If the code hid it unconditionally this would still pass — the pair is the test, the way min-1/min/max/max+1 is the test for a boundary. Add the allow-side case${g.subject.length ? ` for [${g.subject.join(', ')}]` : ''}.`
|
|
766
|
+
: `VISIBILITY-PAIR-MISSING: "${g.scenario}" proves [${g.element}] is ABSENT, and no scenario in this unit proves it is ever SHOWN. "Show A or B, never both" is the defect this misses — and a selector that matches nothing passes \`is hidden\` forever, so a one-sided assertion can be vacuous. Add the counterpart case under the opposite condition (the state where [${g.element}] must appear).`);
|
|
748
767
|
}
|
|
749
768
|
// #630 — a scenario blocked on unseeded data is a DATA task, not unfinished test design.
|
|
750
769
|
{
|
package/src/harness/next-step.ts
CHANGED
|
@@ -167,7 +167,8 @@ const REPAIRABLE = [
|
|
|
167
167
|
'FLOW-CONTRACT-MISSING', 'FLOW-INVENTORY-MISSING', 'FLOW-UNCOVERED', 'FLOW-UNDECLARED',
|
|
168
168
|
'FLOW-PHASE-MISFILED', 'FLOW-GUARANTEE-MISSING', 'FLOW-OUTCOME-UNPROVEN', 'CONTINUITY-ONE-SIDED',
|
|
169
169
|
'ORACLE-URL-ONLY',
|
|
170
|
-
'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', '
|
|
170
|
+
'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', 'SPEC-CLAUSE-UNCOVERED',
|
|
171
|
+
'TRIGGER-UNCOVERED', 'PERMISSION-PAIR-MISSING', 'VISIBILITY-PAIR-MISSING',
|
|
171
172
|
'VIEWPOINT-ITEM-MISSING', 'MANUAL-CODE-MISSING', 'MANUAL-AUTOMATABLE', 'DEPTH-DEFERRED',
|
|
172
173
|
];
|
|
173
174
|
|