@sun-asterisk/sungen 3.2.19 → 3.2.20-beta.2
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 +8 -0
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +7 -0
- 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 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +9 -1
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +11 -0
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +15 -0
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +2 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +1 -0
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/test-data-resolver.d.ts +15 -0
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +34 -1
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +2 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +1 -0
- 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/playwright/playwright-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +4 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +11 -6
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +9 -1
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +10 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +9 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +24 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +53 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +1 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +11 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +20 -0
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts +2 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +72 -9
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/flow-contract.d.ts +71 -0
- package/dist/harness/flow-contract.d.ts.map +1 -0
- package/dist/harness/flow-contract.js +235 -0
- package/dist/harness/flow-contract.js.map +1 -0
- package/dist/harness/flow-plan.d.ts +3 -0
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +6 -2
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/parse.d.ts +5 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +29 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/perf.d.ts +40 -0
- package/dist/harness/perf.d.ts.map +1 -0
- package/dist/harness/perf.js +136 -0
- package/dist/harness/perf.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +9 -1
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/secret-scan.d.ts.map +1 -1
- package/dist/harness/secret-scan.js +13 -1
- package/dist/harness/secret-scan.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +13 -1
- package/dist/harness/sensors.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +41 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +6 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +33 -5
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +35 -16
- package/dist/orchestrator/templates/qa-context.md +14 -1
- package/dist/orchestrator/templates/specs-url-assert.d.ts +67 -0
- package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-url-assert.js +121 -0
- package/dist/orchestrator/templates/specs-url-assert.js.map +1 -0
- package/dist/orchestrator/templates/specs-url-assert.ts +134 -0
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +8 -0
- package/src/cli/commands/delivery.ts +6 -0
- package/src/cli/commands/generate.ts +5 -0
- package/src/exporters/matrix/build.ts +9 -1
- package/src/exporters/matrix/export.ts +11 -0
- package/src/exporters/matrix/render-xlsx.ts +15 -0
- package/src/exporters/matrix/types.ts +2 -0
- package/src/exporters/playwright-report-parser.ts +2 -0
- package/src/exporters/test-data-resolver.ts +34 -1
- package/src/exporters/types.ts +2 -0
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +4 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +11 -6
- package/src/generators/test-generator/code-generator.ts +10 -2
- package/src/generators/test-generator/diagnostics.ts +10 -1
- package/src/generators/test-generator/step-mapper.ts +54 -0
- package/src/generators/test-generator/template-engine.ts +2 -2
- package/src/generators/test-generator/utils/data-resolver.ts +19 -0
- package/src/harness/audit.ts +75 -10
- package/src/harness/flow-contract.ts +229 -0
- package/src/harness/flow-plan.ts +10 -3
- package/src/harness/parse.ts +31 -1
- package/src/harness/perf.ts +112 -0
- package/src/harness/script-check.ts +9 -1
- package/src/harness/secret-scan.ts +12 -1
- package/src/harness/sensors.ts +13 -1
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +41 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +6 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +33 -5
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +35 -16
- package/src/orchestrator/templates/qa-context.md +14 -1
- package/src/orchestrator/templates/specs-url-assert.ts +134 -0
package/src/harness/sensors.ts
CHANGED
|
@@ -33,10 +33,22 @@ const BUCKET_ORDER: Array<[string, string[]]> = [
|
|
|
33
33
|
];
|
|
34
34
|
const BUCKETS: Record<string, string[]> = Object.fromEntries(BUCKET_ORDER);
|
|
35
35
|
|
|
36
|
+
// Flow journey-phase categories (FL-HP-001, FL-ER-002 …). Matched on exact SEGMENTS,
|
|
37
|
+
// never by containment — 'SHOP'.includes('HP') is true, which is exactly the kind of
|
|
38
|
+
// false hit substring matching would produce for two-letter phase tokens. (#569)
|
|
39
|
+
const PHASE_BUCKETS: Record<string, string> = {
|
|
40
|
+
HP: 'business-core', // happy path = the business goal itself
|
|
41
|
+
ER: 'validation-security', // error recovery (validation must not trap the journey)
|
|
42
|
+
EH: 'validation-security', // guards & leakage (direct access, back, refresh)
|
|
43
|
+
};
|
|
44
|
+
|
|
36
45
|
/** Classify a VP category into a balance bucket by keyword containment + precedence (H1). */
|
|
37
46
|
export function bucketForCategory(category: string | undefined): string {
|
|
38
47
|
const cat = (category || '').toUpperCase();
|
|
39
48
|
if (!cat) return 'other';
|
|
49
|
+
for (const seg of cat.split('-')) {
|
|
50
|
+
if (PHASE_BUCKETS[seg]) return PHASE_BUCKETS[seg];
|
|
51
|
+
}
|
|
40
52
|
for (const [bucket, kws] of BUCKET_ORDER) {
|
|
41
53
|
if (kws.some((k) => cat.includes(k))) return bucket;
|
|
42
54
|
}
|
|
@@ -351,7 +363,7 @@ export function flowRegressionDepth(scenarios: ScenarioInfo[]): FlowDepthResult
|
|
|
351
363
|
// 1. Count/quantity proof — a row count or item quantity, not just presence of a row.
|
|
352
364
|
const countProof = any(/\b(quantity|qty|two (?:rows|lines|cart)|row count|count column|number of items|one[_ ]row|two[_ ]rows|qty[_ ])/i);
|
|
353
365
|
// 2. Teardown — removes the item and verifies the empty/zero state (the inverse operation).
|
|
354
|
-
const teardown = any(/\b(remove|delete|clear)
|
|
366
|
+
const teardown = any(/\b(remove|delete|clear)(?:s|d|ed|ing)?\b/i) && any(/\b(empty|emptied|no items|zero|removed|cleared|0 items)\b/i);
|
|
355
367
|
// 3. Multi-source — the cart is fed from >1 source (the main list AND a recommended/related rail).
|
|
356
368
|
const multiSource = any(/\b(recommended|related|you may also|suggest)\b/i) && addsToCart;
|
|
357
369
|
|
|
@@ -86,15 +86,52 @@ qa/flows/${input:flow}/
|
|
|
86
86
|
└── ui/ # Screenshots, mockups
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
### 1a.
|
|
89
|
+
### 1a. Define the flow's BOUNDARY, then its screens
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
A flow is the **smallest complete business action chain**: one clear trigger ending in ONE
|
|
92
|
+
observable, valuable outcome. Before asking for screens, walk this checklist with the user —
|
|
93
|
+
if 1, 3 or 8 fails, propose SPLITTING into separate flows:
|
|
94
|
+
|
|
95
|
+
1. Exactly **one business goal**? (cart correctness + category filtering = two flows)
|
|
96
|
+
2. A clear **trigger** and precondition?
|
|
97
|
+
3. **One observable final outcome**? (a final assertion you can write in one sentence)
|
|
98
|
+
4. Is that outcome **valuable to the actor**? (an order placed, a password reset — not "a page rendered")
|
|
99
|
+
5. Is **every step necessary** for that outcome?
|
|
100
|
+
6. Are all steps at the **same business abstraction**?
|
|
101
|
+
7. Are optional/error branches **phases of this goal** (ER/EH), not new goals?
|
|
102
|
+
8. Does **no segment** form an independently valuable flow on its own?
|
|
103
|
+
9. Can you write **a single clear final assertion**?
|
|
104
|
+
10. Can you name it "**Verb + outcome**"? (`place-order`, `reset-password` — not `cart-and-filter`)
|
|
105
|
+
|
|
106
|
+
Then ask: "Which screens does this flow visit, in order? (e.g., login → dashboard → award-form → confirmation)"
|
|
92
107
|
|
|
93
108
|
Record the screen list — you will need it for:
|
|
94
109
|
- Filling `spec.md` (Step 3)
|
|
95
110
|
- Suggesting `[Screen:Element]` namespace prefixes
|
|
96
111
|
- Capturing visuals per screen (Step 2)
|
|
97
112
|
|
|
113
|
+
### 1b. Author the Flow Contract (`requirements/flow-contract.yaml`)
|
|
114
|
+
|
|
115
|
+
Write the answers down as the flow's contract — `sungen audit` scores the flow **against it**
|
|
116
|
+
(the `flowCoverage` axis: HP/ER/EH journey phases; `FLOW-OUTCOME-UNPROVEN` when no automated
|
|
117
|
+
scenario asserts data on the outcome screen; `FLOW-SCOPE-CREEP` when scenarios never touch it):
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
goal: "Place an order for a product added from home" # Verb + outcome
|
|
121
|
+
actor: user
|
|
122
|
+
trigger: "Add a product to the cart from the home featured list"
|
|
123
|
+
precondition: "A registered account; an empty cart"
|
|
124
|
+
outcome:
|
|
125
|
+
screen: checkout # the [Screen:...] namespace carrying the final proof
|
|
126
|
+
assertion: "The confirmation shows the order number and the paid total"
|
|
127
|
+
value: "The customer has paid; the shop has a new order"
|
|
128
|
+
phases: [HP, ER, EH] # journey phases (default); add UI only if the flow owns UI states
|
|
129
|
+
stateful: cart # the mutated collection, if any — enables regression-depth dims
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**A filled contract is an INPUT to generation — never an output.** Like `test-viewpoint.md`,
|
|
133
|
+
generation must not rewrite it to match what was generated; disagree → propose the diff and ask.
|
|
134
|
+
|
|
98
135
|
### 2. Capture visual source
|
|
99
136
|
|
|
100
137
|
**Mobile path** (`platform: mobile`):
|
|
@@ -187,7 +224,8 @@ If user picks `/sungen:create-test`, **you MUST use the Skill tool** to invoke i
|
|
|
187
224
|
- Test data namespaced by phase: `login.email`, `submission.nominee`
|
|
188
225
|
- `@flow` tag required at feature level
|
|
189
226
|
- `Background:` should only contain the starting navigation — the URL path (web) or the `--reach` nav recipe (mobile)
|
|
190
|
-
- Each scenario = one phase of the journey
|
|
227
|
+
- Each scenario = one phase of the journey; ids are `FL-<PHASE>-NNN` (`HP`/`ER`/`EH`, optional `UI`)
|
|
228
|
+
- One flow = ONE business goal with ONE observable outcome (`requirements/flow-contract.yaml`) — a segment with its own value is its own flow
|
|
191
229
|
{{#cap parallel-subagents}}
|
|
192
230
|
- Mobile flows are tagged `@platform:mobile` and run via `/sungen:run-test <flow>` (WebdriverIO, not Playwright)
|
|
193
231
|
{{/cap}}
|
|
@@ -99,6 +99,12 @@ scheme (`SEC-123`, `PER-345`, `MS-HP-001`) is addressed the same way, and the ca
|
|
|
99
99
|
exist in test-data, so Gate D does not ask for them: the capture step renders as an instruction
|
|
100
100
|
(`Note the X text for later (as "v")`) and later references read `the captured v`.
|
|
101
101
|
|
|
102
|
+
**Dynamic vars**: `{{$timestamp}}`, `{{$uuid}}`, `{{$random:min:max}}`, `{{$date}}`, `{{$datetime}}`
|
|
103
|
+
are resolved by TestDataLoader at execution time — including when nested inside a test-data value
|
|
104
|
+
(`seed: "Summary intro {{$timestamp}}"`). They have no static value to print, so the report names
|
|
105
|
+
them instead: `<unique timestamp>`, `<unique id>`, `<random 1-100>`, `<today's date>`,
|
|
106
|
+
`<current date-time>`. An unknown `{{$foo}}` is left raw on purpose — it is a typo and Gate D says so.
|
|
107
|
+
|
|
102
108
|
**Data fidelity**: invisible test data is made visible, never normalized — `''` → `(empty)`,
|
|
103
109
|
whitespace-only → `(5 spaces)`, padded → `" value "` quoted verbatim. A trim/collapse here would
|
|
104
110
|
silently break the whitespace tests it describes.
|
|
@@ -95,6 +95,10 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
|
|
|
95
95
|
| `SG-W008` | `@manual` scenario incomplete in either direction: (a) HAS Gherkin steps but NO `Then` — a dangling `When` is invalid Gherkin, comments can NOT substitute; (b) documentation-style (NO steps) but its comments lack numbered action lines and/or a labeled oracle line — prose paragraphs / rationale-only comments export EMPTY Steps + Expected cells | Two valid shapes only: complete the Gherkin with a `Then`, or write the FULL procedure in the `# Tester verifies:` block as NUMBERED lines — action steps first (`1. <action>`), then labeled `Observable:`/`Oracle:`/`Expected:` lines (the number is what fills Steps, the label is what fills Expected results) |
|
|
96
96
|
| `SG-W009` | test-data references `${QA_<NAME>}` (a SECRET env ref) but the name is declared in neither `.env.qa` nor `.env.qa.example` — the test throws "environment variable not set" the moment the key is used. (Only the `${QA_*}` shape is checked; any other `${...}` is literal data and never flagged.) | Add `QA_<NAME>=<value>` to `.env.qa` (gitignored) and list the NAME in `.env.qa.example` (the committed checklist). See `qa/TEST-DATA-GUIDE.md` |
|
|
97
97
|
| `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 |
|
|
98
|
+
| `SG-W011` | A storage assertion written as `expect … in local/session storage` — `expect` steps only read `{{response}}` refs, so this compiles to a silent no-op | Rewrite using the supported grammar: `User see [KEY] in local storage exists/…` |
|
|
99
|
+
| `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` |
|
|
100
|
+
| `SG-W013` | A page assertion (`see [X] page` / `is on [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 assertion can never pass | 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) |
|
|
101
|
+
| `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 |
|
|
98
102
|
|
|
99
103
|
### Runtime error → `Test data "<key>" references ${QA_*} but the environment variable is not set`
|
|
100
104
|
|
|
@@ -106,11 +106,39 @@ two content-filtered queries can hit different rows if the table re-renders in b
|
|
|
106
106
|
# 8. Page: User see [T] page | page with {{url}} ← `with` also asserts the query string
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
**Pattern 8 —
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
**Pattern 8 — the page assertion judges pathname AND query together.** Both `see [T] page` and
|
|
110
|
+
`is on [T] page` (Then-side) compile to ONE web-first predicate — `toHaveURL(urlMatches({…}))`,
|
|
111
|
+
which retries like any other assertion — comparing the **exact** pathname (anchored, trailing slash
|
|
112
|
+
normalised) plus the query:
|
|
113
|
+
|
|
114
|
+
| step | asserts |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `Then User see [Search] page` | pathname `/vi/search` **and no query at all** — `/vi/search?x=1` FAILS |
|
|
117
|
+
| `… page with {{v}}`, `v` = `?q=abc` | pathname + **every declared param must match**; extra params are tolerated (a UTM/tracking param cannot turn the test red); a repeated param (`?tag=a&tag=b`) matches per value, so param ORDER never matters |
|
|
118
|
+
| `… page with {{v}}`, `v` = `?q=abc#tab` | the fragment is asserted too |
|
|
119
|
+
| `… page with {{v}}`, `v` = `/vi/search?q=abc` | only the query part is used — the page selector already owns the path |
|
|
120
|
+
| `… page with {{v}}`, `v` = `/vi/search` (no query) | **path only**, and the compiler warns (`SG-W014`): the step asserts less than it reads as |
|
|
121
|
+
| `… page with {{v}}`, `v` empty or key missing at run time | **throws, naming the key** — an unusable value is never a silent pass |
|
|
122
|
+
|
|
123
|
+
So pass `with {{v}}` whenever the URL carries state worth proving, and leave it off when the page
|
|
124
|
+
must be reached clean. A dynamic segment in the selector value (`/users/:id/profile`) wildcards to
|
|
125
|
+
`[^/]+` on its own, still anchored — `/users/42/profile/edit` does not match. Params compare
|
|
126
|
+
decoded on both sides, so a deliberately malformed query still asserts what was written.
|
|
127
|
+
|
|
128
|
+
Two asymmetries worth knowing rather than discovering:
|
|
129
|
+
|
|
130
|
+
1. **`Given`/`When` navigate, `Then` asserts** — so they read `{{v}}` differently. `Then` uses only
|
|
131
|
+
its query/fragment; `Given User is on [T] page with {{v}}` also honours a path the value carries
|
|
132
|
+
(`/vi/other?q=1` navigates to `/vi/other?q=1`, and a value repeating the page's own path no
|
|
133
|
+
longer doubles it). Anything that is not a path, query, or fragment is appended **verbatim** —
|
|
134
|
+
nothing is encoded or normalised, so a test can still drive a deliberately malformed query.
|
|
135
|
+
2. **A repeated param matches as a subset per key**, which is the same rule as "extra params are
|
|
136
|
+
tolerated": every value you declare must be present, and the URL may carry more.
|
|
137
|
+
|
|
138
|
+
The predicate itself lives in `specs/url-assert.ts` (auto-generated, `DO NOT EDIT`). If `[T]` has no
|
|
139
|
+
`type: page` selector entry — or its key collides with a non-page entry, so `value` is something like
|
|
140
|
+
`button` rather than a URL — the step falls back to another path and cannot match the real URL. The
|
|
141
|
+
compiler warns (`SG-W013`); declare the page (or rename the colliding key) instead of chasing the red.
|
|
114
142
|
|
|
115
143
|
### Collection / all-card (P5)
|
|
116
144
|
|
|
@@ -97,7 +97,7 @@ When running Phase 0 for a **flow** (`qa/flows/<name>/`), check existing screen
|
|
|
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
99
|
8. **Merge, don't overwrite**: preserve the page selector and any user-authored entries in `selectors.yaml`. Only add missing keys.
|
|
100
|
-
- **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
|
|
100
|
+
- **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
101
|
9. **Show summary + confirm**: list the keys that will be added, ask the user to approve, then write the file.
|
|
102
102
|
10. **Compile**: **Screen**: `sungen generate --screen <screen>`. **Flow**: `sungen generate --flow <flow>`. Then proceed to Phase 1.
|
|
103
103
|
|
|
@@ -97,12 +97,15 @@ its own `--type` suffix. A key match alone does not carry type: the resolver wal
|
|
|
97
97
|
picked up by any step referencing that label, regardless of the type the step expects.
|
|
98
98
|
|
|
99
99
|
The `page` type is the sharpest trap. A page step (`Then User see [X] page`) reads the entry's
|
|
100
|
-
`value` as a **URL**. If key `X` already holds a `role`/button entry, the
|
|
101
|
-
|
|
100
|
+
`value` as a **URL**. If key `X` already holds a `role`/button entry, the compiler guards the type
|
|
101
|
+
— it does NOT emit `toHaveURL(/button/)` — but it can't invent the real URL either, so it falls
|
|
102
|
+
back to the feature's own path (or `/<ref>/`) and warns `SG-W013`: a test that reads as asserting
|
|
103
|
+
`X`'s page but never can, since the fallback path is anchored and almost certainly wrong.
|
|
102
104
|
|
|
103
105
|
```yaml
|
|
104
106
|
# Gherkin: `When User click [読取結果一覧] button` AND `Then User see [読取結果一覧] page`
|
|
105
|
-
# WRONG — one base key
|
|
107
|
+
# WRONG — one base key: the page step can't use the button's value, falls back to the feature path
|
|
108
|
+
# instead, and the compiler warns SG-W013 rather than asserting the real page:
|
|
106
109
|
読取結果一覧:
|
|
107
110
|
type: 'role'
|
|
108
111
|
value: 'button'
|
|
@@ -605,22 +605,40 @@ error:
|
|
|
605
605
|
|
|
606
606
|
> **Auto-detect**: if path is `qa/flows/<name>/` → use this section. Skip Steps 1–4 above.
|
|
607
607
|
|
|
608
|
+
**Read `requirements/flow-contract.yaml` FIRST — it is the flow's boundary and the yardstick
|
|
609
|
+
`sungen audit` scores the flow against** (`flowCoverage` axis = journey phases HP/ER/EH automated;
|
|
610
|
+
`FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`). No contract yet → author it with the user via the
|
|
611
|
+
boundary checklist in `add-flow` (one business goal · clear trigger · ONE observable outcome
|
|
612
|
+
valuable to the actor · name = "Verb + outcome"), THEN generate. **A filled contract is an INPUT —
|
|
613
|
+
never rewrite it to match your output** (same rule as `test-viewpoint.md`).
|
|
614
|
+
|
|
608
615
|
| Aspect | Screen | Flow |
|
|
609
616
|
|---|---|---|
|
|
610
|
-
| Section focus | UI patterns per section | Journey phases
|
|
617
|
+
| Section focus | UI patterns per section | Journey phases toward ONE declared outcome |
|
|
611
618
|
| Selector format | `[Element]` | `[Screen:Element]` (namespaced) |
|
|
612
619
|
| Test data keys | `{{variable}}` | `{{phase.variable}}` |
|
|
613
620
|
| Feature tag | `@auto` / `@smoke` etc. | `@flow` (required) |
|
|
614
|
-
|
|
|
621
|
+
| Scenario ids | `VP-<CATEGORY>-NNN` | `FL-<PHASE>-NNN` — phases: `HP` (happy path), `ER` (error recovery), `EH` (guards), `UI` (journey UI states, optional) |
|
|
615
622
|
|
|
616
|
-
**Scenarios to generate:**
|
|
623
|
+
**Scenarios to generate — every phase demanded by the contract, automated:**
|
|
617
624
|
|
|
618
|
-
|
|
|
619
|
-
|
|
620
|
-
|
|
|
621
|
-
|
|
|
622
|
-
|
|
|
623
|
-
| Cross-screen
|
|
625
|
+
| Phase | What to test | Scoring |
|
|
626
|
+
|---|---|---|
|
|
627
|
+
| `FL-HP` happy path | The complete journey ending in the contract's `outcome.assertion` — an AUTOMATED **data** assertion on `outcome.screen` (an order number, a summed total — not just "page visible"). This scenario is WHY the flow exists: it is the regression proof after a lib/framework upgrade. | uncovered → `flowCoverage` drops + `FLOW-OUTCOME-UNPROVEN` |
|
|
628
|
+
| `FL-ER` error recovery | Invalid input mid-flow → error shown → fix → the journey still completes. Validation must not trap the journey. | uncovered → `flowCoverage` drops |
|
|
629
|
+
| `FL-EH` guards | Direct URL access without the precondition · browser back · refresh · expired context — each ends in a safe observable state. | uncovered → `flowCoverage` drops |
|
|
630
|
+
| Cross-screen handoff | After every screen transition, assert the CARRIED state on the new screen (the added product's name in the cart, the email echoed on the sent screen). | blind tails cap `businessDepth` (`FLOW-HANDOFF-SHALLOW`) |
|
|
631
|
+
| Stateful regression (when `stateful:` declared) | Count/quantity proof · teardown (remove → empty) · multi-source add. | missing dims cap `businessDepth` (`FLOW-DEPTH`) |
|
|
632
|
+
|
|
633
|
+
**Boundary discipline while generating:** every scenario must serve the contract's goal. A scenario
|
|
634
|
+
that never touches `outcome.screen` and is not a guard (`EH`) or error-recovery (`ER`) belongs in a
|
|
635
|
+
DIFFERENT flow — propose the split instead of writing it here (`FLOW-SCOPE-CREEP` will flag it).
|
|
636
|
+
Auth persistence across transitions is part of `EH` unless the project declares it its own phase.
|
|
637
|
+
|
|
638
|
+
**Manual in flows**: always `@manual:Mx` with the reason code — bare `@manual` is flagged
|
|
639
|
+
(`MANUAL-CODE-MISSING`) because the capability planner cannot route it. Typical flow deferrals:
|
|
640
|
+
inbox/mail oracle → `M5`, network-request count → `M3`, context expiry control → `M7`. A
|
|
641
|
+
cross-screen scenario inside the flow's own goal is NOT manual — automate it here.
|
|
624
642
|
|
|
625
643
|
```gherkin
|
|
626
644
|
@flow @auth:user
|
|
@@ -630,19 +648,20 @@ Feature: Award Submission Flow
|
|
|
630
648
|
Given User is on [Login] page
|
|
631
649
|
|
|
632
650
|
@high
|
|
633
|
-
Scenario:
|
|
651
|
+
Scenario: FL-HP-001 A signed-in user's nomination is submitted and confirmed
|
|
634
652
|
When User fill [Login:Email] field with {{login.email}}
|
|
635
653
|
And User fill [Login:Password] field with {{login.password}}
|
|
636
654
|
And User click [Login:Submit] button
|
|
637
655
|
Then User see [Dashboard] page
|
|
638
|
-
|
|
639
|
-
@high
|
|
640
|
-
Scenario: User submits nomination
|
|
641
656
|
When User click [Dashboard:Awards] link
|
|
642
|
-
|
|
643
|
-
When User fill [Awards:Nominee] field with {{submission.nominee}}
|
|
657
|
+
And User fill [Awards:Nominee] field with {{submission.nominee}}
|
|
644
658
|
And User click [Awards:Submit] button
|
|
645
|
-
Then User see {{success_message}}
|
|
659
|
+
Then User see [Awards:Success Message] text with {{success_message}}
|
|
660
|
+
|
|
661
|
+
@high
|
|
662
|
+
Scenario: FL-EH-001 Direct access to the award form without login redirects to login
|
|
663
|
+
When User go to [Awards] page
|
|
664
|
+
Then User see [Login] page
|
|
646
665
|
```
|
|
647
666
|
|
|
648
667
|
```yaml
|
|
@@ -45,8 +45,21 @@ Example:
|
|
|
45
45
|
|
|
46
46
|
## Testing Strategy
|
|
47
47
|
|
|
48
|
+
Machine-readable intent — `sungen audit` reads these keys (Intent Profile). Values here are
|
|
49
|
+
live even when the surrounding text changes; an invalid value silently falls back to the default.
|
|
50
|
+
|
|
51
|
+
focus: functional
|
|
52
|
+
<!-- focus: functional | e-commerce | security | smoke — drives the audit's depth threshold -->
|
|
53
|
+
|
|
54
|
+
risk_tier: normal
|
|
55
|
+
<!-- risk_tier: high | normal | low -->
|
|
56
|
+
|
|
57
|
+
To silence driver suggestions in audit findings, add a line: capability_suggestions with value off.
|
|
58
|
+
|
|
48
59
|
**Focus areas** — what to cover thoroughly:
|
|
49
|
-
<!--
|
|
60
|
+
<!-- Prose for humans; the parseable value is the `focus:` key above.
|
|
61
|
+
Response-time budgets are NOT a focus value — declare them in qa/perf.yaml
|
|
62
|
+
(percentile + scenario_ms budgets; reported by `sungen delivery`). -->
|
|
50
63
|
<!-- Example: functional, security -->
|
|
51
64
|
|
|
52
65
|
**Mandatory coverage:**
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Sungen URL assertion helper — auto-generated into specs/url-assert.ts. DO NOT EDIT.
|
|
4
|
+
*
|
|
5
|
+
* Every Then-side URL assertion (`see [X] page`, `is on [X] page`) compiles to one call here.
|
|
6
|
+
*
|
|
7
|
+
* Why a helper and not an inline predicate. The predicate used to be emitted inline into every
|
|
8
|
+
* spec, so each correction to what "the URL matches" MEANS was a breaking change for every
|
|
9
|
+
* generated suite in the field: regenerate everything, re-review every newly-red test. Behind a
|
|
10
|
+
* helper the same correction ships as a runtime patch. The generator's job shrinks to what it
|
|
11
|
+
* alone knows — the pathname pattern — and the semantics live in one readable place.
|
|
12
|
+
*
|
|
13
|
+
* Why a predicate and not two assertions. The shape before this one was
|
|
14
|
+
* await expect(page).toHaveURL(/\/vi\/search/); // unanchored: passes on /vi/search-x
|
|
15
|
+
* expect(page.url()).toContain('<query>'); // synchronous, one-shot: never retries
|
|
16
|
+
* — two half-checks, neither anchored, and nothing ever compared pathname and query together.
|
|
17
|
+
* `toHaveURL(predicate)` is web-first (it retries like any other assertion), receives a parsed
|
|
18
|
+
* URL, and needs no regex escaping of runtime values, which is what forced the split.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface UrlExpectation {
|
|
22
|
+
/**
|
|
23
|
+
* Anchored pathname pattern, compiled by the generator from the page selector's `value`
|
|
24
|
+
* (`:id` segments already wildcarded). Anchored against `u.pathname` ONLY: pathname never
|
|
25
|
+
* carries the query, so `^…$` is exact there — the same source that was unsafe to anchor
|
|
26
|
+
* against a full URL.
|
|
27
|
+
*/
|
|
28
|
+
pathname: RegExp;
|
|
29
|
+
/**
|
|
30
|
+
* The `with {{v}}` value, raw from test-data: '?q=1' · '/vi/search?q=1' · '#tab' · ''.
|
|
31
|
+
* `undefined` means the step declared nothing, which is itself an assertion — see below.
|
|
32
|
+
*/
|
|
33
|
+
declared?: string;
|
|
34
|
+
/** The test-data key behind `declared`, so a failure can name what an author must go fix. */
|
|
35
|
+
dataRef?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Join a page's URL with the `with {{v}}` value for a NAVIGATION step
|
|
40
|
+
* (`Given User is on [X] page with {{v}}`).
|
|
41
|
+
*
|
|
42
|
+
* Why this is not a plain `base + path + value`. It used to be, and an author writing the value as a
|
|
43
|
+
* whole relative URL — the habit the assertion side already had to accommodate — navigated to
|
|
44
|
+
* `/vi/search/vi/search?q=1`: the page's own path, twice. The Then side then asserted the real path
|
|
45
|
+
* correctly, so the scenario could never go green.
|
|
46
|
+
*
|
|
47
|
+
* `base` (the configured baseURL, possibly '') and `path` (the page selector's value) arrive
|
|
48
|
+
* SEPARATELY so a value that carries its own rooted path replaces the page's path without also
|
|
49
|
+
* discarding a path prefix baked into the baseURL — a deployment under `https://host/tenant-a`
|
|
50
|
+
* would otherwise navigate to the wrong tenant, and only when a `with {{v}}` was present, which is
|
|
51
|
+
* exactly the kind of inconsistency nobody would think to look for.
|
|
52
|
+
*
|
|
53
|
+
* What it deliberately does NOT do is sanitize. A test may legitimately drive a MALFORMED query to
|
|
54
|
+
* prove the app rejects it (issue #464), so anything that is not a duplicated path is appended
|
|
55
|
+
* verbatim: no encoding, no normalising, no reordering.
|
|
56
|
+
*/
|
|
57
|
+
export function joinPath(base: string, path: string, declared?: string | null): string {
|
|
58
|
+
if (declared === undefined || declared === null || declared === '') return base + path;
|
|
59
|
+
const value = String(declared);
|
|
60
|
+
// A query/fragment appends — the overwhelmingly common shape, and the only one #464 needs.
|
|
61
|
+
if (value.startsWith('?') || value.startsWith('#')) return base + path + value;
|
|
62
|
+
// An absolute URL replaces everything: the author pasted a full address.
|
|
63
|
+
if (/^[a-z]+:\/\//i.test(value)) return value;
|
|
64
|
+
// A rooted path carries its own path, so it replaces the PAGE's path while everything the
|
|
65
|
+
// baseURL contributed — scheme, host, and any path prefix — is kept.
|
|
66
|
+
if (value.startsWith('/')) return base + value;
|
|
67
|
+
// Anything else is raw input the test wants appended as-is.
|
|
68
|
+
return base + path + value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `u.pathname` is percent-encoded; a regex built from a selector's literal `value` is not, so a
|
|
73
|
+
* non-ASCII route (`/読取結果一覧`) could never match. Decoding can throw on a lone `%`, in which
|
|
74
|
+
* case the raw form is all there is.
|
|
75
|
+
*/
|
|
76
|
+
function decodedPath(pathname: string): string {
|
|
77
|
+
try {
|
|
78
|
+
return decodeURIComponent(pathname);
|
|
79
|
+
} catch {
|
|
80
|
+
return pathname;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the predicate for `expect(page).toHaveURL(...)`.
|
|
86
|
+
*
|
|
87
|
+
* Query semantics are deliberately asymmetric:
|
|
88
|
+
* no `with {{v}}` → the URL must carry NO query. A bare page means a bare page, so
|
|
89
|
+
* `/vi/search` does not pass on `/vi/search?x=1`.
|
|
90
|
+
* `with {{v}}` → every declared param must match; extra params are tolerated, so a
|
|
91
|
+
* tracking/UTM param the test never cared about cannot turn it red.
|
|
92
|
+
* A declared fragment is asserted too; a declared value carrying no query
|
|
93
|
+
* at all constrains the path only (the compiler warns: SG-W014).
|
|
94
|
+
*/
|
|
95
|
+
export function urlMatches({ pathname, declared, dataRef }: UrlExpectation): (u: URL) => boolean {
|
|
96
|
+
return (u: URL): boolean => {
|
|
97
|
+
if (!pathname.test(u.pathname) && !pathname.test(decodedPath(u.pathname))) return false;
|
|
98
|
+
|
|
99
|
+
// `dataRef`, not `declared`, decides which rule applies. Both "the step declared nothing" and
|
|
100
|
+
// "the step declared {{v}} but the key resolved to nothing at runtime" arrive here as
|
|
101
|
+
// undefined, and they mean opposite things: the first is a real assertion (a bare page must be
|
|
102
|
+
// bare), the second is broken test-data. Only the generator knows which, and it says so by
|
|
103
|
+
// emitting `dataRef` exactly when the step carried a `with {{v}}`.
|
|
104
|
+
if (!dataRef) return u.search === '';
|
|
105
|
+
|
|
106
|
+
// Silence is the whole bug class this assertion exists to kill, so an unusable declared value
|
|
107
|
+
// is an ERROR, not a quiet verdict. Returning false would read as "the app is wrong"; these
|
|
108
|
+
// throws say "your test-data is wrong, and here is the key". An empty value used to parse to
|
|
109
|
+
// an empty param list, and `[].every()` is vacuously TRUE — it passed on any query at all.
|
|
110
|
+
if (declared === undefined || declared === null) {
|
|
111
|
+
throw new Error(`url assertion: test-data key "${dataRef}" is missing — nothing to assert`);
|
|
112
|
+
}
|
|
113
|
+
if (String(declared).trim() === '') {
|
|
114
|
+
throw new Error(`url assertion: test-data key "${dataRef}" is empty — nothing to assert`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Parsing against the live pathname as base lets ONE expression handle every shape an author
|
|
118
|
+
// writes: '?q=1' (query only), '/vi/search?q=1' (a whole relative URL — its path is ignored,
|
|
119
|
+
// the page selector already owns the path), and '#tab' (fragment only). The predecessor
|
|
120
|
+
// regex-stripped the prefix instead, which silently turned a value with NO query
|
|
121
|
+
// ('/vi/search') into a bogus param NAME — an assertion that could never be satisfied.
|
|
122
|
+
const d = new URL(String(declared), 'http://sungen.invalid' + u.pathname);
|
|
123
|
+
|
|
124
|
+
// getAll, not get: a multi-select filter (?tag=a&tag=b) is the exact case this assertion is
|
|
125
|
+
// for, and `get()` returns only the first value — so the correct URL failed. Comparing as a
|
|
126
|
+
// subset per key keeps this consistent with "extra params are tolerated", and makes param
|
|
127
|
+
// order meaningless, which is what a URL actually means.
|
|
128
|
+
const queryOk = [...d.searchParams].every(([k, v]) => u.searchParams.getAll(k).includes(v));
|
|
129
|
+
|
|
130
|
+
// Only assert a fragment the author actually declared.
|
|
131
|
+
const hashOk = !d.hash || d.hash === u.hash;
|
|
132
|
+
return queryOk && hashOk;
|
|
133
|
+
};
|
|
134
|
+
}
|