@sun-asterisk/sungen 3.2.21 → 3.2.22-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 +3 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +15 -0
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/inspect.d.ts +41 -0
- package/dist/cli/commands/inspect.d.ts.map +1 -0
- package/dist/cli/commands/inspect.js +134 -0
- package/dist/cli/commands/inspect.js.map +1 -0
- package/dist/cli/commands/trace.d.ts.map +1 -1
- package/dist/cli/commands/trace.js +9 -0
- package/dist/cli/commands/trace.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +10 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +38 -0
- 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 +44 -1
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +10 -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/types.d.ts +2 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
- package/dist/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +29 -8
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +25 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +24 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +45 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +159 -20
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +36 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +33 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +100 -24
- package/dist/generators/test-generator/step-mapper.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 +101 -10
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/flow-contract.d.ts +87 -0
- package/dist/harness/flow-contract.d.ts.map +1 -0
- package/dist/harness/flow-contract.js +259 -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/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +13 -1
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +8 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +60 -6
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +51 -3
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +10 -0
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +23 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
- 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 +61 -12
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +65 -17
- package/dist/orchestrator/templates/qa-context.md +14 -1
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +104 -29
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +104 -26
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +18 -5
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +19 -5
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +8 -0
- package/src/cli/commands/delivery.ts +14 -2
- package/src/cli/commands/inspect.ts +128 -0
- package/src/cli/commands/trace.ts +9 -0
- package/src/cli/index.ts +2 -0
- package/src/exporters/matrix/build.ts +40 -0
- package/src/exporters/matrix/export.ts +11 -0
- package/src/exporters/matrix/render-xlsx.ts +45 -1
- package/src/exporters/matrix/types.ts +10 -0
- package/src/exporters/playwright-report-parser.ts +2 -0
- package/src/exporters/types.ts +2 -0
- package/src/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
- package/src/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
- package/src/generators/test-generator/code-generator.ts +34 -9
- package/src/generators/test-generator/diagnostics.ts +25 -1
- package/src/generators/test-generator/patterns/index.ts +172 -25
- package/src/generators/test-generator/patterns/types.ts +35 -0
- package/src/generators/test-generator/step-mapper.ts +106 -23
- package/src/harness/audit.ts +104 -11
- package/src/harness/flow-contract.ts +261 -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/sensors.ts +13 -1
- package/src/harness/spec-coverage.ts +55 -5
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +51 -3
- package/src/orchestrator/templates/ai-src/commands/create-test.md +10 -0
- package/src/orchestrator/templates/ai-src/commands/run-test.md +23 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
- 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 +61 -12
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +65 -17
- package/src/orchestrator/templates/qa-context.md +14 -1
- package/src/orchestrator/templates/specs-api.ts +104 -26
- package/src/orchestrator/templates/specs-db.ts +19 -5
|
@@ -479,7 +479,11 @@ Add cleanup tags per the `sungen-gherkin-syntax` Cleanup table. Key rules:
|
|
|
479
479
|
|
|
480
480
|
**Files:** `qa/screens/<screen>/features/<screen>.feature` + `qa/screens/<screen>/test-data/<screen>.yaml`
|
|
481
481
|
|
|
482
|
-
Use step patterns and element types from `sungen-gherkin-syntax`.
|
|
482
|
+
Use step patterns and element types from `sungen-gherkin-syntax`. For a `@platform:mobile/android/ios`
|
|
483
|
+
screen or flow, first check that skill's **Platform Support** section — never author a step marked
|
|
484
|
+
`[web]` there (tables, browser storage, tab order, `@mock`, uploader, `Then User is on [T] page`) for
|
|
485
|
+
a mobile unit, and read the divergence table before relying on `see [T] page` or `hover` to prove
|
|
486
|
+
anything on mobile. Reach for `sungen-mobile-gestures` for the gesture vocabulary instead.
|
|
483
487
|
**Naming**: reuse the **project's `test-viewpoint.md` IDs** when it declares them (e.g. `VP0`, `MS-HP-001`); otherwise `VP-<CATEGORY>-<NNN>`. Scenario name must use the **same element type** as the steps.
|
|
484
488
|
|
|
485
489
|
**Test data** — grouped by section, loaded at runtime:
|
|
@@ -603,24 +607,67 @@ error:
|
|
|
603
607
|
|
|
604
608
|
## Flow Test Generation
|
|
605
609
|
|
|
610
|
+
> **Terminology**: many QA teams call this level **System Test** (ISTQB: verify the fully
|
|
611
|
+
> integrated system against its specified requirements, designed around business
|
|
612
|
+
> processes/use cases). In sungen a *flow* is that system-test suite run through the real UI;
|
|
613
|
+
> its automation then serves as the regression suite on later runs. The names are
|
|
614
|
+
> interchangeable in conversation; the artifacts stay `qa/flows/<name>/`.
|
|
615
|
+
|
|
616
|
+
**Representative, not exhaustive (ISTQB system-test design rule).** A flow case takes ONE
|
|
617
|
+
representative input per branch — one valid value for the main flow, one error value that
|
|
618
|
+
triggers the exception branch and proves the journey recovers. The full equivalence/boundary
|
|
619
|
+
matrix belongs to the OWNING SCREEN (as `@cases` there), never re-run inside the flow: if the
|
|
620
|
+
screen already proved "the field rejects negatives", the flow only proves "the journey survives
|
|
621
|
+
one rejected input". `sungen audit` reports `FLOW-EXHAUSTIVE-INPUTS` when ≥3 same-shape flow
|
|
622
|
+
scenarios differ only in data. **Mocked dependencies**: a flow scenario using `@mock` is
|
|
623
|
+
delivered as *Pass (Mocked)* — verified handling logic, not the real dependency; the delivery's
|
|
624
|
+
External Dependency Tracking table lists it until re-verified real.
|
|
625
|
+
|
|
626
|
+
**Release selection & external legs**: when the contract declares `golden: true`, tag the
|
|
627
|
+
happy-path scenario(s) that prove the outcome with **`@golden`** — that is the set
|
|
628
|
+
`sungen inspect` (Final Inspection, Go/No-Go) runs before a release; `GOLDEN-MISSING` fires
|
|
629
|
+
until it exists, and a @golden scenario that passes only against mocks BLOCKS the release.
|
|
630
|
+
When the contract declares `external:` legs (another team/vendor's system), scenarios touching
|
|
631
|
+
those namespaces are the **System INTEGRATION** group — keep their oracles about the boundary
|
|
632
|
+
(contract/format/handoff), and expect `SIT-MOCK-ONLY` until they are re-verified real.
|
|
633
|
+
|
|
634
|
+
|
|
606
635
|
> **Auto-detect**: if path is `qa/flows/<name>/` → use this section. Skip Steps 1–4 above.
|
|
607
636
|
|
|
637
|
+
**Read `requirements/flow-contract.yaml` FIRST — it is the flow's boundary and the yardstick
|
|
638
|
+
`sungen audit` scores the flow against** (`flowCoverage` axis = journey phases HP/ER/EH automated;
|
|
639
|
+
`FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`). No contract yet → author it with the user via the
|
|
640
|
+
boundary checklist in `add-flow` (one business goal · clear trigger · ONE observable outcome
|
|
641
|
+
valuable to the actor · name = "Verb + outcome"), THEN generate. **A filled contract is an INPUT —
|
|
642
|
+
never rewrite it to match your output** (same rule as `test-viewpoint.md`).
|
|
643
|
+
|
|
608
644
|
| Aspect | Screen | Flow |
|
|
609
645
|
|---|---|---|
|
|
610
|
-
| Section focus | UI patterns per section | Journey phases
|
|
646
|
+
| Section focus | UI patterns per section | Journey phases toward ONE declared outcome |
|
|
611
647
|
| Selector format | `[Element]` | `[Screen:Element]` (namespaced) |
|
|
612
648
|
| Test data keys | `{{variable}}` | `{{phase.variable}}` |
|
|
613
649
|
| Feature tag | `@auto` / `@smoke` etc. | `@flow` (required) |
|
|
614
|
-
|
|
|
650
|
+
| Scenario ids | `VP-<CATEGORY>-NNN` | `FL-<PHASE>-NNN` — phases: `HP` (happy path), `ER` (error recovery), `EH` (guards), `UI` (journey UI states, optional) |
|
|
615
651
|
|
|
616
|
-
**Scenarios to generate:**
|
|
652
|
+
**Scenarios to generate — every phase demanded by the contract, automated:**
|
|
617
653
|
|
|
618
|
-
|
|
|
619
|
-
|
|
620
|
-
|
|
|
621
|
-
|
|
|
622
|
-
|
|
|
623
|
-
| Cross-screen
|
|
654
|
+
| Phase | What to test | Scoring |
|
|
655
|
+
|---|---|---|
|
|
656
|
+
| `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` |
|
|
657
|
+
| `FL-ER` error recovery | Invalid input mid-flow → error shown → fix → the journey still completes. Validation must not trap the journey. | uncovered → `flowCoverage` drops |
|
|
658
|
+
| `FL-EH` guards | Direct URL access without the precondition · browser back · refresh · expired context — each ends in a safe observable state. | uncovered → `flowCoverage` drops |
|
|
659
|
+
| 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`) |
|
|
660
|
+
| Stateful regression (when `stateful:` declared) | Count/quantity proof · teardown (remove → empty) · multi-source add. | missing dims cap `businessDepth` (`FLOW-DEPTH`) |
|
|
661
|
+
|
|
662
|
+
**Boundary discipline while generating:** every scenario must serve the contract's goal. A scenario
|
|
663
|
+
that never touches `outcome.screen` and is not a guard (`EH`) or error-recovery (`ER`) belongs in a
|
|
664
|
+
DIFFERENT flow — propose the split instead of writing it here (`FLOW-SCOPE-CREEP` will flag it).
|
|
665
|
+
Auth persistence across transitions is part of `EH` unless the project declares it its own phase.
|
|
666
|
+
|
|
667
|
+
**Manual in flows**: always `@manual:Mx` with the reason code — bare `@manual` is flagged
|
|
668
|
+
(`MANUAL-CODE-MISSING`) because the capability planner cannot route it. Typical flow deferrals:
|
|
669
|
+
inbox/mail oracle → `M5`, network-request count → `M3`, context expiry control → `M7`. A
|
|
670
|
+
cross-screen scenario inside the flow's own goal is NOT manual — automate it here.
|
|
624
671
|
|
|
625
672
|
```gherkin
|
|
626
673
|
@flow @auth:user
|
|
@@ -630,19 +677,20 @@ Feature: Award Submission Flow
|
|
|
630
677
|
Given User is on [Login] page
|
|
631
678
|
|
|
632
679
|
@high
|
|
633
|
-
Scenario:
|
|
680
|
+
Scenario: FL-HP-001 A signed-in user's nomination is submitted and confirmed
|
|
634
681
|
When User fill [Login:Email] field with {{login.email}}
|
|
635
682
|
And User fill [Login:Password] field with {{login.password}}
|
|
636
683
|
And User click [Login:Submit] button
|
|
637
684
|
Then User see [Dashboard] page
|
|
638
|
-
|
|
639
|
-
@high
|
|
640
|
-
Scenario: User submits nomination
|
|
641
685
|
When User click [Dashboard:Awards] link
|
|
642
|
-
|
|
643
|
-
When User fill [Awards:Nominee] field with {{submission.nominee}}
|
|
686
|
+
And User fill [Awards:Nominee] field with {{submission.nominee}}
|
|
644
687
|
And User click [Awards:Submit] button
|
|
645
|
-
Then User see {{success_message}}
|
|
688
|
+
Then User see [Awards:Success Message] text with {{success_message}}
|
|
689
|
+
|
|
690
|
+
@high
|
|
691
|
+
Scenario: FL-EH-001 Direct access to the award form without login redirects to login
|
|
692
|
+
When User go to [Awards] page
|
|
693
|
+
Then User see [Login] page
|
|
646
694
|
```
|
|
647
695
|
|
|
648
696
|
```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:**
|
|
@@ -7,12 +7,16 @@
|
|
|
7
7
|
* `{{name.body.<path>}}`. Base URL + auth come from a `kind: api` datasource in datasources.yaml,
|
|
8
8
|
* with `${VAR}` resolved from .env.qa / process.env — never inline.
|
|
9
9
|
*
|
|
10
|
+
* Runs on Node's global `fetch` (Node 18+) — no `@playwright/test` import, so this driver loads in
|
|
11
|
+
* mobile (Appium/Mocha) specs, which carry no Playwright dependency. Previously built on
|
|
12
|
+
* `APIRequestContext`; ported method-by-method (see `ApiClient.call`) to keep behaviour — including
|
|
13
|
+
* the `@hybrid` storageState cookie reuse — equivalent on web.
|
|
14
|
+
*
|
|
10
15
|
* Safety: a datasource flagged `env: production` is refused unless SUNGEN_ALLOW_PROD=1.
|
|
11
16
|
* DO NOT EDIT — regenerated by `sungen generate`.
|
|
12
17
|
*/
|
|
13
18
|
import * as fs from 'fs';
|
|
14
19
|
import * as path from 'path';
|
|
15
|
-
import { request, type APIRequestContext } from '@playwright/test';
|
|
16
20
|
|
|
17
21
|
interface ApiDataSource {
|
|
18
22
|
kind?: string;
|
|
@@ -56,11 +60,53 @@ function substituteRaw(text: string, params: Record<string, any>): string {
|
|
|
56
60
|
}
|
|
57
61
|
/** Delete any case-insensitive variant of a header (HTTP header names are case-insensitive, but a
|
|
58
62
|
* plain object keys on exact case — so a datasource `Content-Type` and our `content-type` would
|
|
59
|
-
* both be sent). Call before setting, or to let
|
|
63
|
+
* both be sent). Call before setting, or to let fetch own the header (multipart boundary). */
|
|
60
64
|
function deleteHeader(headers: Record<string, string>, name: string): void {
|
|
61
65
|
const lower = name.toLowerCase();
|
|
62
66
|
for (const k of Object.keys(headers)) if (k.toLowerCase() === lower) delete headers[k];
|
|
63
67
|
}
|
|
68
|
+
/** True when `headers` already carries a case-insensitive match for `name` — used to respect an
|
|
69
|
+
* explicit catalog header instead of overriding it with our own content-type default. */
|
|
70
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
71
|
+
const lower = name.toLowerCase();
|
|
72
|
+
return Object.keys(headers).some((k) => k.toLowerCase() === lower);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build a `Cookie:` header value from a Playwright storageState JSON file (written by
|
|
77
|
+
* `sungen makeauth` / `context.storageState()` — `{ cookies: [...], origins: [...] }`), scoped to
|
|
78
|
+
* the cookies that apply to `url` the way a browser would send them (domain + path + not expired).
|
|
79
|
+
* Powers `@hybrid`: the API call reuses the UI session's cookies without a browser/Playwright
|
|
80
|
+
* context. Returns undefined when the file has no cookie that matches `url`.
|
|
81
|
+
*/
|
|
82
|
+
function loadStorageStateCookieHeader(storageStatePath: string, url: string): string | undefined {
|
|
83
|
+
const abs = path.isAbsolute(storageStatePath) ? storageStatePath : path.join(process.cwd(), storageStatePath);
|
|
84
|
+
if (!fs.existsSync(abs)) {
|
|
85
|
+
throw new Error(`API Driver: storageState "${storageStatePath}" not found — run \`sungen makeauth\` first.`);
|
|
86
|
+
}
|
|
87
|
+
const state = JSON.parse(fs.readFileSync(abs, 'utf-8')) as {
|
|
88
|
+
cookies?: Array<{ name: string; value: string; domain?: string; path?: string; expires?: number; secure?: boolean }>;
|
|
89
|
+
};
|
|
90
|
+
const cookies = state.cookies || [];
|
|
91
|
+
if (!cookies.length) return undefined;
|
|
92
|
+
const target = new URL(url);
|
|
93
|
+
const nowSec = Date.now() / 1000;
|
|
94
|
+
const targetIsSecure = target.protocol === 'https:';
|
|
95
|
+
const matches = cookies.filter((c) => {
|
|
96
|
+
const domain = (c.domain || '').replace(/^\./, '');
|
|
97
|
+
const domainOk = !domain || target.hostname === domain || target.hostname.endsWith(`.${domain}`);
|
|
98
|
+
const pathOk = target.pathname.startsWith(c.path || '/');
|
|
99
|
+
const notExpired = c.expires === undefined || c.expires < 0 || c.expires > nowSec;
|
|
100
|
+
// A Secure cookie must never leave over plaintext. Playwright's own cookie jar enforces this,
|
|
101
|
+
// and this function exists to be EQUIVALENT to it — omitting the check would make a mixed
|
|
102
|
+
// @hybrid setup (https browser session, http API datasource) leak the session cookie in clear,
|
|
103
|
+
// a downgrade introduced by us rather than by anything the author wrote.
|
|
104
|
+
const secureOk = !c.secure || targetIsSecure;
|
|
105
|
+
return domainOk && pathOk && notExpired && secureOk;
|
|
106
|
+
});
|
|
107
|
+
if (!matches.length) return undefined;
|
|
108
|
+
return matches.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
109
|
+
}
|
|
64
110
|
|
|
65
111
|
/** A multipart file field embedded by the compiler (string = shorthand for { path }). */
|
|
66
112
|
type FileSpec = string | { path: string; mimeType?: string; filename?: string; name?: string };
|
|
@@ -173,11 +219,13 @@ class ApiClient {
|
|
|
173
219
|
if (req.body !== undefined && req.body !== null) {
|
|
174
220
|
body = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
175
221
|
}
|
|
176
|
-
// Map the wire format to
|
|
177
|
-
// default), form →
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
222
|
+
// Map the wire format to a fetch request body (#345): json → JSON string (application/json,
|
|
223
|
+
// default), form → URL-encoded string, multipart → FormData. A `files` block is a real upload:
|
|
224
|
+
// read each fixture into a Blob part and send multipart, with any `body` values riding along
|
|
225
|
+
// as text fields. (Typed as `any`, not fetch's DOM-only `BodyInit`: this package's tsconfig has
|
|
226
|
+
// no "dom" lib, so that alias isn't in scope even though the global runtime values are.)
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- see comment above
|
|
228
|
+
let fetchBody: any;
|
|
181
229
|
if (req.bodyFile) {
|
|
182
230
|
// Raw-binary body: the whole request body is the fixture's bytes. Content-Type comes from the
|
|
183
231
|
// spec's mimeType (default application/octet-stream) — this is what octet-stream upload
|
|
@@ -186,40 +234,70 @@ class ApiClient {
|
|
|
186
234
|
const fpath = substituteRaw(String(spec.path), params);
|
|
187
235
|
if (fpath) {
|
|
188
236
|
const resolved = resolveFixture(fpath, label);
|
|
189
|
-
|
|
237
|
+
fetchBody = fs.readFileSync(resolved);
|
|
190
238
|
// Own the Content-Type: drop any datasource default (case-insensitive) so we don't send both.
|
|
191
239
|
deleteHeader(headers, 'content-type');
|
|
192
240
|
headers['content-type'] = spec.mimeType ?? 'application/octet-stream';
|
|
193
241
|
}
|
|
194
242
|
} else if (req.files && Object.keys(req.files).length) {
|
|
195
|
-
//
|
|
196
|
-
// default Content-Type so it doesn't conflict with / duplicate
|
|
243
|
+
// fetch derives the multipart/form-data Content-Type (with boundary) from the FormData body;
|
|
244
|
+
// drop any datasource default Content-Type so it doesn't conflict with / duplicate it.
|
|
197
245
|
deleteHeader(headers, 'content-type');
|
|
198
|
-
|
|
246
|
+
fetchBody = buildMultipart(req.files, body, params, label);
|
|
199
247
|
} else if (body !== undefined) {
|
|
200
248
|
const enc = req.encoding ?? 'json';
|
|
201
|
-
if (enc === 'form')
|
|
202
|
-
|
|
203
|
-
|
|
249
|
+
if (enc === 'form') {
|
|
250
|
+
if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
251
|
+
fetchBody = new URLSearchParams(body as Record<string, string>).toString();
|
|
252
|
+
} else if (enc === 'multipart') {
|
|
253
|
+
deleteHeader(headers, 'content-type');
|
|
254
|
+
fetchBody = buildMultipart({}, body, params, label); // no file fields — text fields only
|
|
255
|
+
} else {
|
|
256
|
+
if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/json';
|
|
257
|
+
fetchBody = JSON.stringify(body);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// GET/HEAD requests may not carry a body under fetch (undici throws). Refuse LOUDLY rather than
|
|
262
|
+
// send a request quietly shorter than the catalog says it is — a dropped body reads as a server
|
|
263
|
+
// bug, not an authoring one. `validateApiEntry` catches this at generate time; this is the
|
|
264
|
+
// backstop for a catalog that reached the runtime some other way.
|
|
265
|
+
if (fetchBody !== undefined && /^(GET|HEAD)$/i.test(req.method)) {
|
|
266
|
+
throw new Error(`API Driver: ${label} — a ${req.method.toUpperCase()} request cannot carry a body. Remove body:/files:/bodyFile: from the catalog entry, or change its method to POST/PUT/PATCH.`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Full URL (not a baseURL-relative path) so a base path component like /api/v3 is preserved.
|
|
270
|
+
const fullUrl = joinApiUrl(base, urlPath);
|
|
271
|
+
|
|
272
|
+
// @hybrid: `storageState` is the @auth role's saved session (Playwright's `context.storageState()`
|
|
273
|
+
// JSON) — reuse its cookies (scoped to `fullUrl`'s domain/path) so the request shares the
|
|
274
|
+
// browser's authenticated session without a browser/Playwright context.
|
|
275
|
+
if (opts.storageState) {
|
|
276
|
+
const cookieHeader = loadStorageStateCookieHeader(opts.storageState, fullUrl);
|
|
277
|
+
if (cookieHeader) {
|
|
278
|
+
const existingKey = Object.keys(headers).find((k) => k.toLowerCase() === 'cookie');
|
|
279
|
+
headers[existingKey || 'cookie'] = existingKey ? `${headers[existingKey]}; ${cookieHeader}` : cookieHeader;
|
|
280
|
+
}
|
|
204
281
|
}
|
|
205
282
|
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
timeout: req.timeout ?? conf.timeout_ms ?? 15000, // per-endpoint override → datasource → 15s
|
|
212
|
-
...(opts.storageState ? { storageState: opts.storageState } : {}),
|
|
213
|
-
});
|
|
283
|
+
// Timeout: fetch has no built-in per-request timeout, so abort it manually — mirrors the prior
|
|
284
|
+
// per-endpoint-override → datasource → 15s default precedence.
|
|
285
|
+
const timeoutMs = req.timeout ?? conf.timeout_ms ?? 15000;
|
|
286
|
+
const controller = new AbortController();
|
|
287
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
214
288
|
try {
|
|
215
|
-
|
|
216
|
-
|
|
289
|
+
const res = await fetch(fullUrl, {
|
|
290
|
+
method: req.method,
|
|
291
|
+
headers,
|
|
292
|
+
...(fetchBody !== undefined ? { body: fetchBody } : {}),
|
|
293
|
+
signal: controller.signal,
|
|
294
|
+
});
|
|
217
295
|
const text = await res.text();
|
|
218
296
|
let parsed: any = text;
|
|
219
297
|
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
220
|
-
return { status: res.status
|
|
298
|
+
return { status: res.status, ok: res.ok, body: parsed, headers: Object.fromEntries(res.headers.entries()) };
|
|
221
299
|
} finally {
|
|
222
|
-
|
|
300
|
+
clearTimeout(timer);
|
|
223
301
|
}
|
|
224
302
|
}
|
|
225
303
|
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
* Engines: PostgreSQL (`pg`), MySQL (`mysql2`), and SQLite (`better-sqlite3`), lazy-loaded on first use.
|
|
10
10
|
* Config: a `datasources.yaml` at the project root (or qa/), with ${VAR} resolved from env.
|
|
11
11
|
*
|
|
12
|
+
* Assertions are plain thrown Errors — no `@playwright/test` import — so this helper loads in
|
|
13
|
+
* mobile (Appium/Mocha) specs too, which carry no Playwright dependency (same reason
|
|
14
|
+
* `specs/api.ts` runs on Node's global `fetch`).
|
|
15
|
+
*
|
|
12
16
|
* DO NOT EDIT — regenerated by `sungen generate`.
|
|
13
17
|
*/
|
|
14
|
-
import { expect } from '@playwright/test';
|
|
15
18
|
import * as fs from 'fs';
|
|
16
19
|
import * as path from 'path';
|
|
17
20
|
|
|
@@ -647,12 +650,13 @@ class DataSource {
|
|
|
647
650
|
async assertRow(table: string, filter: Record<string, any>, expected?: Record<string, any>, datasource?: string): Promise<void> {
|
|
648
651
|
const { engine } = await this.engine(datasource);
|
|
649
652
|
const rows = await engine.findRows(table, filter, 50);
|
|
650
|
-
|
|
653
|
+
assert(rows.length >= 1, `Expected a row in "${table}" where ${desc(filter)} — found ${rows.length}`);
|
|
651
654
|
if (expected) {
|
|
652
655
|
const row = rows[0];
|
|
653
656
|
for (const [col, val] of Object.entries(expected)) {
|
|
654
657
|
ident(col);
|
|
655
|
-
|
|
658
|
+
assert(String(row[col]) === String(val),
|
|
659
|
+
`Column "${col}" of "${table}" where ${desc(filter)} — expected ${JSON.stringify(String(val))}, found ${JSON.stringify(String(row[col]))}`);
|
|
656
660
|
}
|
|
657
661
|
}
|
|
658
662
|
}
|
|
@@ -661,7 +665,7 @@ class DataSource {
|
|
|
661
665
|
async assertNoRow(table: string, filter: Record<string, any>, datasource?: string): Promise<void> {
|
|
662
666
|
const { engine } = await this.engine(datasource);
|
|
663
667
|
const rows = await engine.findRows(table, filter, 50);
|
|
664
|
-
|
|
668
|
+
assert(rows.length === 0, `Expected NO row in "${table}" where ${desc(filter)} — found ${rows.length}`);
|
|
665
669
|
}
|
|
666
670
|
|
|
667
671
|
/** Exactly `count` rows must match `filter`. */
|
|
@@ -669,7 +673,7 @@ class DataSource {
|
|
|
669
673
|
const { engine } = await this.engine(datasource);
|
|
670
674
|
const n = await engine.countRows(table, filter);
|
|
671
675
|
const cols = Object.keys(filter);
|
|
672
|
-
|
|
676
|
+
assert(n === Number(count), `Expected ${count} row(s) in "${table}"${cols.length ? ' where ' + desc(filter) : ''} — found ${n}`);
|
|
673
677
|
}
|
|
674
678
|
|
|
675
679
|
/**
|
|
@@ -694,6 +698,16 @@ class DataSource {
|
|
|
694
698
|
}
|
|
695
699
|
}
|
|
696
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Fail a DB assertion. A thrown Error is exactly what BOTH runners report as a test failure
|
|
703
|
+
* (Playwright and Mocha/WDIO), so this replaces Playwright's `expect(...)` without importing it —
|
|
704
|
+
* the coupling that kept `specs/db.ts` out of mobile specs. Messages carry expected AND found,
|
|
705
|
+
* since there is no matcher to print them.
|
|
706
|
+
*/
|
|
707
|
+
function assert(cond: boolean, message: string): void {
|
|
708
|
+
if (!cond) throw new Error(`[sungen] ${message}`);
|
|
709
|
+
}
|
|
710
|
+
|
|
697
711
|
function desc(filter: Record<string, any>): string {
|
|
698
712
|
return Object.entries(filter).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(', ');
|
|
699
713
|
}
|