@sun-asterisk/sungen 2.4.6 → 2.5.1
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/README.md +88 -7
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +109 -9
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/figma.d.ts +11 -0
- package/dist/cli/commands/figma.d.ts.map +1 -0
- package/dist/cli/commands/figma.js +178 -0
- package/dist/cli/commands/figma.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +2 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/index.js +4 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +1 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +3 -0
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +29 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +21 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js +11 -2
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
- package/dist/generators/test-generator/code-generator.d.ts +2 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +109 -12
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +1 -1
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +29 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +11 -2
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +11 -2
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +36 -25
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +7 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +42 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -0
- package/dist/generators/types.d.ts +1 -0
- package/dist/generators/types.d.ts.map +1 -1
- package/dist/generators/types.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +2 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/figma/figma-scaffolder-helpers.d.ts +33 -0
- package/dist/orchestrator/figma/figma-scaffolder-helpers.d.ts.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder-helpers.js +135 -0
- package/dist/orchestrator/figma/figma-scaffolder-helpers.js.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder-types.d.ts +25 -0
- package/dist/orchestrator/figma/figma-scaffolder-types.d.ts.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder-types.js +7 -0
- package/dist/orchestrator/figma/figma-scaffolder-types.js.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder.d.ts +23 -0
- package/dist/orchestrator/figma/figma-scaffolder.d.ts.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder.js +212 -0
- package/dist/orchestrator/figma/figma-scaffolder.js.map +1 -0
- package/dist/orchestrator/figma/node-path-collapser.d.ts +16 -0
- package/dist/orchestrator/figma/node-path-collapser.d.ts.map +1 -0
- package/dist/orchestrator/figma/node-path-collapser.js +37 -0
- package/dist/orchestrator/figma/node-path-collapser.js.map +1 -0
- package/dist/orchestrator/figma/spec-figma-renderer.d.ts +44 -0
- package/dist/orchestrator/figma/spec-figma-renderer.d.ts.map +1 -0
- package/dist/orchestrator/figma/spec-figma-renderer.js +45 -0
- package/dist/orchestrator/figma/spec-figma-renderer.js.map +1 -0
- package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts +23 -0
- package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts.map +1 -0
- package/dist/orchestrator/figma/spec-figma-section-renderers.js +47 -0
- package/dist/orchestrator/figma/spec-figma-section-renderers.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts +9 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +74 -10
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +56 -11
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +30 -17
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +4 -3
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +34 -2
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +12 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-figma-source.md +151 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +93 -23
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +53 -9
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +21 -16
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +4 -3
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +34 -2
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
- package/dist/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +151 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +151 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +61 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +105 -28
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +20 -0
- package/dist/orchestrator/templates/specs-base.d.ts +12 -1
- package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-base.js +47 -5
- package/dist/orchestrator/templates/specs-base.js.map +1 -1
- package/dist/orchestrator/templates/specs-base.ts +65 -7
- package/dist/orchestrator/templates/specs-test-data.d.ts +14 -0
- package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-test-data.js +100 -0
- package/dist/orchestrator/templates/specs-test-data.js.map +1 -0
- package/dist/orchestrator/templates/specs-test-data.ts +66 -0
- package/dist/tools/figma/figma-auth.d.ts +36 -0
- package/dist/tools/figma/figma-auth.d.ts.map +1 -0
- package/dist/tools/figma/figma-auth.js +182 -0
- package/dist/tools/figma/figma-auth.js.map +1 -0
- package/dist/tools/figma/figma-cache.d.ts +45 -0
- package/dist/tools/figma/figma-cache.d.ts.map +1 -0
- package/dist/tools/figma/figma-cache.js +191 -0
- package/dist/tools/figma/figma-cache.js.map +1 -0
- package/dist/tools/figma/figma-client-types.d.ts +112 -0
- package/dist/tools/figma/figma-client-types.d.ts.map +1 -0
- package/dist/tools/figma/figma-client-types.js +7 -0
- package/dist/tools/figma/figma-client-types.js.map +1 -0
- package/dist/tools/figma/figma-errors.d.ts +49 -0
- package/dist/tools/figma/figma-errors.d.ts.map +1 -0
- package/dist/tools/figma/figma-errors.js +105 -0
- package/dist/tools/figma/figma-errors.js.map +1 -0
- package/dist/tools/figma/figma-image-downloader.d.ts +25 -0
- package/dist/tools/figma/figma-image-downloader.d.ts.map +1 -0
- package/dist/tools/figma/figma-image-downloader.js +128 -0
- package/dist/tools/figma/figma-image-downloader.js.map +1 -0
- package/dist/tools/figma/figma-node-filter.d.ts +26 -0
- package/dist/tools/figma/figma-node-filter.d.ts.map +1 -0
- package/dist/tools/figma/figma-node-filter.js +164 -0
- package/dist/tools/figma/figma-node-filter.js.map +1 -0
- package/dist/tools/figma/figma-rest-client.d.ts +24 -0
- package/dist/tools/figma/figma-rest-client.d.ts.map +1 -0
- package/dist/tools/figma/figma-rest-client.js +154 -0
- package/dist/tools/figma/figma-rest-client.js.map +1 -0
- package/dist/tools/figma/figma-url-parser.d.ts +18 -0
- package/dist/tools/figma/figma-url-parser.d.ts.map +1 -0
- package/dist/tools/figma/figma-url-parser.js +51 -0
- package/dist/tools/figma/figma-url-parser.js.map +1 -0
- package/dist/utils/exec-file-no-throw.d.ts +20 -0
- package/dist/utils/exec-file-no-throw.d.ts.map +1 -0
- package/dist/utils/exec-file-no-throw.js +36 -0
- package/dist/utils/exec-file-no-throw.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/add.ts +80 -9
- package/src/cli/commands/figma.ts +162 -0
- package/src/cli/commands/generate.ts +2 -0
- package/src/cli/index.ts +4 -2
- package/src/generators/gherkin-parser/index.ts +4 -0
- package/src/generators/test-generator/adapters/adapter-interface.ts +12 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +14 -2
- package/src/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
- package/src/generators/test-generator/code-generator.ts +122 -13
- package/src/generators/test-generator/step-mapper.ts +2 -2
- package/src/generators/test-generator/template-engine.ts +28 -2
- package/src/generators/test-generator/utils/data-resolver.ts +45 -27
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +51 -0
- package/src/generators/types.ts +1 -0
- package/src/orchestrator/ai-rules-updater.ts +2 -0
- package/src/orchestrator/figma/figma-scaffolder-helpers.ts +126 -0
- package/src/orchestrator/figma/figma-scaffolder-types.ts +26 -0
- package/src/orchestrator/figma/figma-scaffolder.ts +209 -0
- package/src/orchestrator/figma/node-path-collapser.ts +38 -0
- package/src/orchestrator/figma/spec-figma-renderer.ts +80 -0
- package/src/orchestrator/figma/spec-figma-section-renderers.ts +46 -0
- package/src/orchestrator/project-initializer.ts +84 -10
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +56 -11
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +30 -17
- package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +4 -3
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +34 -2
- package/src/orchestrator/templates/ai-instructions/claude-config.md +12 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-figma-source.md +151 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +93 -23
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +2 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +53 -9
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +21 -16
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +4 -3
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +34 -2
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
- package/src/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +151 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +151 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +61 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +105 -28
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +20 -0
- package/src/orchestrator/templates/specs-base.ts +65 -7
- package/src/orchestrator/templates/specs-test-data.ts +66 -0
- package/src/tools/figma/figma-auth.ts +161 -0
- package/src/tools/figma/figma-cache.ts +184 -0
- package/src/tools/figma/figma-client-types.ts +125 -0
- package/src/tools/figma/figma-errors.ts +127 -0
- package/src/tools/figma/figma-image-downloader.ts +112 -0
- package/src/tools/figma/figma-node-filter.ts +198 -0
- package/src/tools/figma/figma-rest-client.ts +183 -0
- package/src/tools/figma/figma-url-parser.ts +55 -0
- package/src/utils/exec-file-no-throw.ts +45 -0
|
@@ -6,14 +6,25 @@ user-invocable: false
|
|
|
6
6
|
|
|
7
7
|
## Goal
|
|
8
8
|
|
|
9
|
-
Generate **focused test cases per screen section**
|
|
9
|
+
Generate **focused test cases per screen section** using a **tier-based approach** for faster results. Output `.feature` + `test-data.yaml` only — selectors are deferred to `/sungen:run-test`.
|
|
10
|
+
|
|
11
|
+
### Tier System
|
|
12
|
+
|
|
13
|
+
| Tier | Priority | What to generate | When |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| **Tier 1** (default) | `@critical` + `@high` | Happy paths, required validation, core business rules, security basics | First run of `create-test` |
|
|
16
|
+
| **Tier 2** (expand) | `@normal` + `@low` | UI presence, optional validation, edge cases, cosmetic checks | User runs `create-test` again with "Add viewpoints" mode |
|
|
17
|
+
|
|
18
|
+
**Round 1 (Tier 1)** targets **~10-15 scenarios per section** — enough to cover critical flows and catch real bugs. This is the default behavior.
|
|
19
|
+
|
|
20
|
+
**Round 2 (Tier 2)** expands to full coverage when the user explicitly chooses "Add viewpoints" or "Add new sections" update mode. Only then generate `@normal` + `@low` scenarios to fill coverage gaps.
|
|
10
21
|
|
|
11
22
|
## Update Mode
|
|
12
23
|
|
|
13
24
|
When `.feature` already has scenarios, summarize and ask:
|
|
14
|
-
1. **Add new sections** — append, continue numbering
|
|
15
|
-
2. **Add viewpoints** —
|
|
16
|
-
3. **Replace all** — overwrite
|
|
25
|
+
1. **Add new sections** — append new sections with Tier 2 (`@normal` + `@low`) scenarios, continue numbering
|
|
26
|
+
2. **Add viewpoints** — expand existing sections with Tier 2 (`@normal` + `@low`) scenarios
|
|
27
|
+
3. **Replace all** — overwrite with fresh Tier 1 (`@critical` + `@high`) generation
|
|
17
28
|
|
|
18
29
|
For append: read highest `VP-<CAT>-<NNN>`, continue from next number. Never modify existing scenarios.
|
|
19
30
|
|
|
@@ -28,14 +39,28 @@ Requirements improve every viewpoint: exact error messages for VAL, business rul
|
|
|
28
39
|
|
|
29
40
|
If also exploring live page: verify spec vs actual, flag mismatches, capture exact text.
|
|
30
41
|
|
|
42
|
+
### Figma supplement (`spec_figma.md`)
|
|
43
|
+
|
|
44
|
+
When `requirements/spec_figma.md` is present alongside `spec.md`, treat it as a **secondary input** with these rules:
|
|
45
|
+
|
|
46
|
+
- **Never override `spec.md`**: `spec.md` is authoritative for all business rules, field constraints, and behavior. `spec_figma.md` only supplements with visual/text data that `spec.md` may lack.
|
|
47
|
+
- **`## Text Inventory` → literal strings**: use text label values from this section verbatim in `test-data.yaml` (button labels, input placeholders, error messages shown in Figma). Do not paraphrase or invent alternatives.
|
|
48
|
+
- **`## Interaction States` → state coverage checkpoints**: use the listed variants (e.g., empty, loading, error, success) as a checklist for state-coverage scenarios. Only generate scenarios for states that are either (a) confirmed in both `spec.md` and `spec_figma.md`, or (b) explicitly documented in one source without contradiction from the other.
|
|
49
|
+
- **Flag disagreements**: if a field name, label, or behavior in `spec_figma.md` contradicts `spec.md`, insert an HTML comment at the top of the `.feature` file:
|
|
50
|
+
```gherkin
|
|
51
|
+
<!-- FIGMA-SPEC CONFLICT: <brief description> — using spec.md value -->
|
|
52
|
+
```
|
|
53
|
+
Then proceed using the `spec.md` value.
|
|
54
|
+
|
|
31
55
|
## Screen Input Sources
|
|
32
56
|
|
|
33
|
-
**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
57
|
+
**Auto-detect** — the parent command (`create-test`) resolves visual sources before invoking this skill. By the time generation starts, the available sources are already determined:
|
|
58
|
+
- `spec.md` — primary, always read if present
|
|
59
|
+
- `spec_figma.md` — Figma supplement, read if present (PAT flow already completed)
|
|
60
|
+
- `ui/*.png` — visual context, read if present
|
|
61
|
+
- `test-viewpoint.md` — edge cases and known issues, read if present
|
|
37
62
|
|
|
38
|
-
**
|
|
63
|
+
**IMPORTANT:** If `spec_figma.md` exists, do NOT call any `mcp__figma__*` tool. The PAT flow is complete — just read the file.
|
|
39
64
|
|
|
40
65
|
**Single screen focus**: one URL = one screen. Don't explore sibling paths. Modals on same page = part of this screen.
|
|
41
66
|
|
|
@@ -76,7 +101,9 @@ Apply `sungen-test-design-techniques` to spec-extracted conditions:
|
|
|
76
101
|
|
|
77
102
|
Use `sungen-viewpoint` skill for per-pattern checklists across 4 viewpoints: UI/UX, Data & Validate, Logic, Security.
|
|
78
103
|
|
|
79
|
-
|
|
104
|
+
**Tier-aware gap filling:**
|
|
105
|
+
- **Tier 1 (first run)**: only add `@critical` and `@high` items from the checklists — core security checks (VP-SEC), required field validation (VP-VAL), key state transitions (VP-LOGIC). Skip `@normal`/`@low` items like hover states, empty states, tooltips.
|
|
106
|
+
- **Tier 2 (expand run)**: add `@normal` + `@low` scenarios — UI presence, optional validation, edge cases, cosmetic checks, keyboard nav, hover effects.
|
|
80
107
|
|
|
81
108
|
**Validation rule**: capture actual error messages from live page or spec.md. Use `User see {{error_var}}` — never assert just "is visible".
|
|
82
109
|
|
|
@@ -112,30 +139,70 @@ Given User is on [Screen] page
|
|
|
112
139
|
And User wait for [Page Title] heading is visible
|
|
113
140
|
```
|
|
114
141
|
|
|
142
|
+
## Cleanup & Hooks
|
|
143
|
+
|
|
144
|
+
### Auto-assign `@cleanup:*` tags based on screen sections
|
|
145
|
+
|
|
146
|
+
After identifying screen sections, add appropriate `@cleanup:*` feature-level tags. These activate base.ts fixtures that auto-clean state between tests.
|
|
147
|
+
|
|
148
|
+
| Screen has | Add tag | Why |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| Modal / Dialog / Drawer | `@cleanup:overlay` | Dismiss leftover overlays between tests |
|
|
151
|
+
| Form & Inputs / Search / Filter | `@cleanup:forms` | Clear form fields, reset selects |
|
|
152
|
+
| Long scrollable content | `@cleanup:scroll` | Scroll to top for consistent assertions |
|
|
153
|
+
| Auth tokens / session data in tests | `@cleanup:storage` | Clear sessionStorage |
|
|
154
|
+
| CI/CD or debug-heavy screens | `@screenshot:on-failure` | Auto-capture screenshot on test failure |
|
|
155
|
+
|
|
156
|
+
**Always add `@cleanup:overlay`** if ANY section opens a dialog (Create/Add, Update/Edit, Delete confirmation). Most CRUD screens need it.
|
|
157
|
+
|
|
158
|
+
**Always add `@cleanup:forms`** if the screen has inline search, filter dropdowns, or editable forms that persist between tests.
|
|
159
|
+
|
|
160
|
+
### When to add `@afterEach` hook scenario
|
|
161
|
+
|
|
162
|
+
Only when `@cleanup:*` tags aren't enough — feature-specific cleanup logic:
|
|
163
|
+
- Reset a dropdown filter to default value (not just clear)
|
|
164
|
+
- Navigate away from a sub-tab back to the main tab
|
|
165
|
+
- Close a specific sidebar panel
|
|
166
|
+
|
|
167
|
+
```gherkin
|
|
168
|
+
@afterEach
|
|
169
|
+
Scenario: Reset filters to default
|
|
170
|
+
When User select [Status Filter] dropdown with {{default_status}}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `@beforeAll` / `@afterAll` — optional, low priority
|
|
174
|
+
|
|
175
|
+
For one-time setup/teardown. Most screens don't need these.
|
|
176
|
+
|
|
115
177
|
## Output Format
|
|
116
178
|
|
|
117
179
|
**Feature file** — `qa/screens/<screen>/features/<screen>.feature`
|
|
118
180
|
|
|
119
|
-
|
|
181
|
+
`Background` is valid for simple shared setup (navigate to page). Use `@steps`/`@extend` for complex flows with scope (dialog, frame).
|
|
120
182
|
|
|
121
183
|
```gherkin
|
|
122
184
|
@auth:role
|
|
185
|
+
@cleanup:overlay
|
|
186
|
+
@cleanup:forms
|
|
123
187
|
Feature: <Screen> Screen
|
|
124
188
|
|
|
189
|
+
Background:
|
|
190
|
+
Given User is on [Screen] page
|
|
191
|
+
|
|
125
192
|
# Shared setup — NO priority tag on @steps
|
|
126
193
|
@steps:open_form
|
|
127
194
|
Scenario: Open form
|
|
128
|
-
Given User is on [Screen] page
|
|
129
195
|
When User click [Create] button
|
|
130
196
|
Then User see [Form] dialog
|
|
131
197
|
|
|
132
|
-
# --- Section: Create User Form ---
|
|
198
|
+
# --- Section: Create User Form (Tier 1: @critical + @high) ---
|
|
133
199
|
|
|
134
|
-
@
|
|
135
|
-
Scenario: VP-
|
|
200
|
+
@critical @extend:open_form
|
|
201
|
+
Scenario: VP-LOGIC-001 Submit form with valid data creates record
|
|
136
202
|
Given User is on [Form] dialog
|
|
137
|
-
|
|
138
|
-
And User
|
|
203
|
+
When User fill [Name] field with {{valid_name}}
|
|
204
|
+
And User click [Submit] button
|
|
205
|
+
Then User see {{success_message}} message
|
|
139
206
|
|
|
140
207
|
@high @extend:open_form
|
|
141
208
|
Scenario: VP-VAL-001 Submit with all empty fields shows errors
|
|
@@ -143,11 +210,7 @@ Feature: <Screen> Screen
|
|
|
143
210
|
When User click [Submit] button
|
|
144
211
|
Then User see [Name error] message with {{name_required_error}}
|
|
145
212
|
|
|
146
|
-
# --- Section: User Table ---
|
|
147
|
-
|
|
148
|
-
@normal
|
|
149
|
-
Scenario: VP-UI-010 Table displays all columns
|
|
150
|
-
Then User see [Name] column in [Users] table
|
|
213
|
+
# --- Section: User Table (Tier 1: @critical + @high) ---
|
|
151
214
|
|
|
152
215
|
@high
|
|
153
216
|
Scenario: VP-VAL-010 Table displays correct data
|
|
@@ -155,13 +218,15 @@ Feature: <Screen> Screen
|
|
|
155
218
|
| Name | Email |
|
|
156
219
|
| {{name_1}} | {{email_1}} |
|
|
157
220
|
|
|
158
|
-
@
|
|
159
|
-
Scenario: VP-
|
|
160
|
-
Given User
|
|
161
|
-
When User
|
|
162
|
-
Then User
|
|
221
|
+
@critical
|
|
222
|
+
Scenario: VP-SEC-010 Unauthorized user cannot access page
|
|
223
|
+
Given User is not logged in
|
|
224
|
+
When User navigate to [Screen] page
|
|
225
|
+
Then User is on [Login] page
|
|
163
226
|
```
|
|
164
227
|
|
|
228
|
+
**Tier 2 (expand run)** adds `@normal` + `@low` scenarios like UI field presence, hover states, tooltips, empty states.
|
|
229
|
+
|
|
165
230
|
### When to use DataTable vs Row Scope
|
|
166
231
|
|
|
167
232
|
| Pattern | Use when |
|
|
@@ -171,6 +236,18 @@ Feature: <Screen> Screen
|
|
|
171
236
|
|
|
172
237
|
**Naming**: `VP-<CATEGORY>-<NNN>` prefix. Scenario name must use the **same element type** as the steps — e.g., if the step uses `dialog`, write "dialog opens" not "modal opens".
|
|
173
238
|
|
|
174
|
-
**Test data** — `qa/screens/<screen>/test-data/<screen>.yaml`, grouped by section.
|
|
239
|
+
**Test data** — `qa/screens/<screen>/test-data/<screen>.yaml`, grouped by section. Data is loaded **at runtime** — keys become runtime lookups, not hardcoded strings. The same compiled test works across environments.
|
|
240
|
+
|
|
241
|
+
**Environment-specific data**: For values that differ per environment (credentials, URLs, test users), create `<screen>.<env>.yaml` alongside the base file. Users run `SUNGEN_ENV=staging npx playwright test` to merge overrides. Structure env YAML with the same keys, only including values that change:
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
# login.yaml (base)
|
|
245
|
+
valid_email: admin@dev.example.com
|
|
246
|
+
valid_password: DevPass123
|
|
247
|
+
|
|
248
|
+
# login.staging.yaml (override for staging)
|
|
249
|
+
valid_email: admin@staging.example.com
|
|
250
|
+
valid_password: StagingPass456
|
|
251
|
+
```
|
|
175
252
|
|
|
176
253
|
**Do NOT generate**: `selectors.yaml` (created during run-test), Playwright code (sungen compiles).
|
|
@@ -43,6 +43,8 @@ Score: `(dimensions_covered / 6) * 40`. Validate technique application with `sun
|
|
|
43
43
|
|
|
44
44
|
**Classification**: UI = static/always-same appearance. VAL = input validation/errors. LOGIC = behavior/state changes (includes persisted state without When). SEC = auth/permissions.
|
|
45
45
|
|
|
46
|
+
**Tier-aware scoring**: If the feature file only contains `@critical` + `@high` scenarios (Tier 1), do NOT penalize for missing VP-UI viewpoint — UI scenarios are intentionally deferred to Tier 2. Score "All applicable VP present" based on Tier 1-relevant viewpoints only (VAL, LOGIC, SEC). Note in the review output: *"VP-UI deferred to Tier 2 — run create-test with 'Add viewpoints' to expand."*
|
|
47
|
+
|
|
46
48
|
---
|
|
47
49
|
|
|
48
50
|
## Quality Rules
|
|
@@ -87,6 +89,21 @@ Do NOT mark `@manual` when data is visible in snapshot or documented in spec —
|
|
|
87
89
|
|
|
88
90
|
---
|
|
89
91
|
|
|
92
|
+
## Unverified Selectors metric
|
|
93
|
+
|
|
94
|
+
**When to check**: if `qa/screens/<screen>/selectors/<screen>.yaml` exists, count lines matching the pattern `@needs-live-verify`.
|
|
95
|
+
|
|
96
|
+
| Metric | Source | Scoring impact |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Unverified Selectors | Count of `@needs-live-verify` comment lines in `selectors.yaml` | None — non-blocking |
|
|
99
|
+
|
|
100
|
+
- If count > 0: include the following line in the review report summary (after the score table, before Issues):
|
|
101
|
+
`⚠ <N> selectors flagged @needs-live-verify — run run-test against live URL when available.`
|
|
102
|
+
- Does NOT reduce the score or block the 60% threshold. The suite can PASS with unverified selectors.
|
|
103
|
+
- If `selectors.yaml` does not exist yet (not yet generated): omit this metric entirely.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
90
107
|
## Output Format
|
|
91
108
|
|
|
92
109
|
```markdown
|
|
@@ -99,6 +116,9 @@ Do NOT mark `@manual` when data is visible in snapshot or documented in spec —
|
|
|
99
116
|
| Viewpoint | <n> | 30 |
|
|
100
117
|
| **Total** | **<n>%** | **100** |
|
|
101
118
|
|
|
119
|
+
⚠ <N> selectors flagged @needs-live-verify — run run-test against live URL when available.
|
|
120
|
+
<!-- omit this line if selectors.yaml doesn't exist or @needs-live-verify count = 0 -->
|
|
121
|
+
|
|
102
122
|
### Issues
|
|
103
123
|
1. [SYNTAX] ...
|
|
104
124
|
2. [COVERAGE] ...
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { expect } from '@playwright/test';
|
|
2
|
-
|
|
2
|
+
type CleanupConfig = {
|
|
3
|
+
overlay?: boolean;
|
|
4
|
+
forms?: boolean;
|
|
5
|
+
scroll?: boolean;
|
|
6
|
+
storage?: boolean;
|
|
7
|
+
};
|
|
8
|
+
declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
|
|
9
|
+
autoCleanup: CleanupConfig;
|
|
10
|
+
screenshotOnFailure: boolean;
|
|
11
|
+
_autoCleanup: void;
|
|
12
|
+
_autoScreenshot: void;
|
|
13
|
+
}, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
3
14
|
export { test, expect };
|
|
4
15
|
//# sourceMappingURL=specs-base.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-base.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"specs-base.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGxD,KAAK,aAAa,GAAG;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAOF,QAAA,MAAM,IAAI;iBACK,aAAa;yBACL,OAAO;kBACd,IAAI;qBACD,IAAI;wGA6GrB,CAAC;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -8,6 +8,8 @@ Object.defineProperty(exports, "expect", { enumerable: true, get: function () {
|
|
|
8
8
|
const contextCache = new Map();
|
|
9
9
|
const GOTO_PATCHED = Symbol('goto-patched');
|
|
10
10
|
const test = test_1.test.extend({
|
|
11
|
+
autoCleanup: [{}, { option: true }],
|
|
12
|
+
screenshotOnFailure: [false, { option: true }],
|
|
11
13
|
page: async ({ browser, storageState }, use) => {
|
|
12
14
|
if (storageState) {
|
|
13
15
|
const cacheKey = typeof storageState === 'string' ? storageState : JSON.stringify(storageState);
|
|
@@ -26,11 +28,6 @@ const test = test_1.test.extend({
|
|
|
26
28
|
try {
|
|
27
29
|
const currentPath = new URL(page.url()).pathname;
|
|
28
30
|
if (currentPath === url || currentPath === url + '/') {
|
|
29
|
-
// Dismiss any open overlays (dropdowns, dialogs) from previous test
|
|
30
|
-
await page.keyboard.press('Escape').catch(() => { });
|
|
31
|
-
await page.locator('body').click({ position: { x: 1, y: 1 }, force: true }).catch(() => { });
|
|
32
|
-
// Safety check: if a fixed-position overlay (modal/dialog) is still present, full reload
|
|
33
|
-
// eslint-disable-next-line no-eval -- runs in browser context via Playwright
|
|
34
31
|
const hasOverlay = await page.evaluate(`(() => {
|
|
35
32
|
const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
|
|
36
33
|
if (!el) return false;
|
|
@@ -65,6 +62,51 @@ const test = test_1.test.extend({
|
|
|
65
62
|
await context.close();
|
|
66
63
|
}
|
|
67
64
|
},
|
|
65
|
+
// Auto-cleanup fixture: runs teardown after each test based on @cleanup:* tags
|
|
66
|
+
_autoCleanup: [async ({ page, autoCleanup }, use) => {
|
|
67
|
+
await use();
|
|
68
|
+
if (autoCleanup.overlay) {
|
|
69
|
+
await page.keyboard.press('Escape').catch(() => { });
|
|
70
|
+
await page.locator('body').click({ position: { x: 1, y: 1 }, force: true }).catch(() => { });
|
|
71
|
+
// Dismiss persistent fixed overlays (modals, dialogs)
|
|
72
|
+
const hasOverlay = await page.evaluate(`(() => {
|
|
73
|
+
const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
|
|
74
|
+
if (!el) return false;
|
|
75
|
+
let current = el;
|
|
76
|
+
while (current && current !== document.body) {
|
|
77
|
+
if (getComputedStyle(current).position === 'fixed') return true;
|
|
78
|
+
current = current.parentElement;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
})()`).catch(() => false);
|
|
82
|
+
if (hasOverlay) {
|
|
83
|
+
await page.keyboard.press('Escape').catch(() => { });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (autoCleanup.forms) {
|
|
87
|
+
await page.evaluate(`(() => {
|
|
88
|
+
document.querySelectorAll('input:not([type=hidden]):not([type=submit])').forEach(el => { el.value = ''; });
|
|
89
|
+
document.querySelectorAll('textarea').forEach(el => { el.value = ''; });
|
|
90
|
+
document.querySelectorAll('select').forEach(el => { el.selectedIndex = 0; });
|
|
91
|
+
})()`).catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
if (autoCleanup.scroll) {
|
|
94
|
+
await page.evaluate('window.scrollTo(0, 0)').catch(() => { });
|
|
95
|
+
}
|
|
96
|
+
if (autoCleanup.storage) {
|
|
97
|
+
await page.evaluate('sessionStorage.clear()').catch(() => { });
|
|
98
|
+
}
|
|
99
|
+
}, { auto: true }],
|
|
100
|
+
// Auto-screenshot fixture: captures screenshot on test failure
|
|
101
|
+
_autoScreenshot: [async ({ page, screenshotOnFailure }, use, testInfo) => {
|
|
102
|
+
await use();
|
|
103
|
+
if (screenshotOnFailure && testInfo.status !== testInfo.expectedStatus) {
|
|
104
|
+
await testInfo.attach('screenshot', {
|
|
105
|
+
body: await page.screenshot(),
|
|
106
|
+
contentType: 'image/png',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}, { auto: true }],
|
|
68
110
|
});
|
|
69
111
|
exports.test = test;
|
|
70
112
|
//# sourceMappingURL=specs-base.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-base.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-base.ts"],"names":[],"mappings":";;;AAAA,2CAAwD;
|
|
1
|
+
{"version":3,"file":"specs-base.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-base.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { test as base, expect } from '@playwright/test';
|
|
2
2
|
import type { BrowserContext, Page } from '@playwright/test';
|
|
3
3
|
|
|
4
|
+
type CleanupConfig = {
|
|
5
|
+
overlay?: boolean;
|
|
6
|
+
forms?: boolean;
|
|
7
|
+
scroll?: boolean;
|
|
8
|
+
storage?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
4
11
|
// Share one context per storageState — avoids creating multiple sessions
|
|
5
12
|
// that trigger server rate limiting or session invalidation
|
|
6
13
|
const contextCache = new Map<string, { context: BrowserContext; page: Page }>();
|
|
7
14
|
const GOTO_PATCHED = Symbol('goto-patched');
|
|
8
15
|
|
|
9
|
-
const test = base.extend
|
|
16
|
+
const test = base.extend<{
|
|
17
|
+
autoCleanup: CleanupConfig;
|
|
18
|
+
screenshotOnFailure: boolean;
|
|
19
|
+
_autoCleanup: void;
|
|
20
|
+
_autoScreenshot: void;
|
|
21
|
+
}>({
|
|
22
|
+
autoCleanup: [{}, { option: true }],
|
|
23
|
+
screenshotOnFailure: [false, { option: true }],
|
|
24
|
+
|
|
10
25
|
page: async ({ browser, storageState }, use) => {
|
|
11
26
|
if (storageState) {
|
|
12
27
|
const cacheKey = typeof storageState === 'string' ? storageState : JSON.stringify(storageState);
|
|
@@ -28,12 +43,6 @@ const test = base.extend({
|
|
|
28
43
|
try {
|
|
29
44
|
const currentPath = new URL(page.url()).pathname;
|
|
30
45
|
if (currentPath === url || currentPath === url + '/') {
|
|
31
|
-
// Dismiss any open overlays (dropdowns, dialogs) from previous test
|
|
32
|
-
await page.keyboard.press('Escape').catch(() => {});
|
|
33
|
-
await page.locator('body').click({ position: { x: 1, y: 1 }, force: true }).catch(() => {});
|
|
34
|
-
|
|
35
|
-
// Safety check: if a fixed-position overlay (modal/dialog) is still present, full reload
|
|
36
|
-
// eslint-disable-next-line no-eval -- runs in browser context via Playwright
|
|
37
46
|
const hasOverlay = await page.evaluate(`(() => {
|
|
38
47
|
const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
|
|
39
48
|
if (!el) return false;
|
|
@@ -68,6 +77,55 @@ const test = base.extend({
|
|
|
68
77
|
await context.close();
|
|
69
78
|
}
|
|
70
79
|
},
|
|
80
|
+
|
|
81
|
+
// Auto-cleanup fixture: runs teardown after each test based on @cleanup:* tags
|
|
82
|
+
_autoCleanup: [async ({ page, autoCleanup }, use) => {
|
|
83
|
+
await use();
|
|
84
|
+
|
|
85
|
+
if (autoCleanup.overlay) {
|
|
86
|
+
await page.keyboard.press('Escape').catch(() => {});
|
|
87
|
+
await page.locator('body').click({ position: { x: 1, y: 1 }, force: true }).catch(() => {});
|
|
88
|
+
// Dismiss persistent fixed overlays (modals, dialogs)
|
|
89
|
+
const hasOverlay = await page.evaluate(`(() => {
|
|
90
|
+
const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
|
|
91
|
+
if (!el) return false;
|
|
92
|
+
let current = el;
|
|
93
|
+
while (current && current !== document.body) {
|
|
94
|
+
if (getComputedStyle(current).position === 'fixed') return true;
|
|
95
|
+
current = current.parentElement;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
})()`).catch(() => false);
|
|
99
|
+
if (hasOverlay) {
|
|
100
|
+
await page.keyboard.press('Escape').catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (autoCleanup.forms) {
|
|
104
|
+
await page.evaluate(`(() => {
|
|
105
|
+
document.querySelectorAll('input:not([type=hidden]):not([type=submit])').forEach(el => { el.value = ''; });
|
|
106
|
+
document.querySelectorAll('textarea').forEach(el => { el.value = ''; });
|
|
107
|
+
document.querySelectorAll('select').forEach(el => { el.selectedIndex = 0; });
|
|
108
|
+
})()`).catch(() => {});
|
|
109
|
+
}
|
|
110
|
+
if (autoCleanup.scroll) {
|
|
111
|
+
await page.evaluate('window.scrollTo(0, 0)').catch(() => {});
|
|
112
|
+
}
|
|
113
|
+
if (autoCleanup.storage) {
|
|
114
|
+
await page.evaluate('sessionStorage.clear()').catch(() => {});
|
|
115
|
+
}
|
|
116
|
+
}, { auto: true }],
|
|
117
|
+
|
|
118
|
+
// Auto-screenshot fixture: captures screenshot on test failure
|
|
119
|
+
_autoScreenshot: [async ({ page, screenshotOnFailure }, use, testInfo) => {
|
|
120
|
+
await use();
|
|
121
|
+
|
|
122
|
+
if (screenshotOnFailure && testInfo.status !== testInfo.expectedStatus) {
|
|
123
|
+
await testInfo.attach('screenshot', {
|
|
124
|
+
body: await page.screenshot(),
|
|
125
|
+
contentType: 'image/png',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}, { auto: true }],
|
|
71
129
|
});
|
|
72
130
|
|
|
73
131
|
export { test, expect };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class TestDataLoader {
|
|
2
|
+
private data;
|
|
3
|
+
private constructor();
|
|
4
|
+
/**
|
|
5
|
+
* Load test data for a screen/feature combination.
|
|
6
|
+
*
|
|
7
|
+
* Priority (later wins):
|
|
8
|
+
* 1. {feature}.yaml — base data
|
|
9
|
+
* 2. {feature}.{SUNGEN_ENV}.yaml — environment-specific (if SUNGEN_ENV set)
|
|
10
|
+
*/
|
|
11
|
+
static load(screenName: string, featureName: string): TestDataLoader;
|
|
12
|
+
get(key: string): string;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=specs-test-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specs-test-data.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-test-data.ts"],"names":[],"mappings":"AAIA,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAsB;IAElC,OAAO;IAIP;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc;IAcpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;CAczB"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.TestDataLoader = void 0;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
43
|
+
class TestDataLoader {
|
|
44
|
+
constructor(data) {
|
|
45
|
+
this.data = data;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Load test data for a screen/feature combination.
|
|
49
|
+
*
|
|
50
|
+
* Priority (later wins):
|
|
51
|
+
* 1. {feature}.yaml — base data
|
|
52
|
+
* 2. {feature}.{SUNGEN_ENV}.yaml — environment-specific (if SUNGEN_ENV set)
|
|
53
|
+
*/
|
|
54
|
+
static load(screenName, featureName) {
|
|
55
|
+
const baseDir = path.join(process.cwd(), 'qa', 'screens', screenName, 'test-data');
|
|
56
|
+
const env = process.env.SUNGEN_ENV;
|
|
57
|
+
let data = loadYamlSync(path.join(baseDir, `${featureName}.yaml`)) || {};
|
|
58
|
+
if (env) {
|
|
59
|
+
const envData = loadYamlSync(path.join(baseDir, `${featureName}.${env}.yaml`));
|
|
60
|
+
if (envData)
|
|
61
|
+
data = deepMerge(data, envData);
|
|
62
|
+
}
|
|
63
|
+
return new TestDataLoader(data);
|
|
64
|
+
}
|
|
65
|
+
get(key) {
|
|
66
|
+
const parts = key.split('.');
|
|
67
|
+
let current = this.data;
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
if (current == null || typeof current !== 'object') {
|
|
70
|
+
throw new Error(`Test data key not found: ${key} (failed at '${part}')`);
|
|
71
|
+
}
|
|
72
|
+
current = current[part];
|
|
73
|
+
}
|
|
74
|
+
if (current === undefined || current === null) {
|
|
75
|
+
throw new Error(`Test data key not found: ${key}`);
|
|
76
|
+
}
|
|
77
|
+
return String(current);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.TestDataLoader = TestDataLoader;
|
|
81
|
+
function loadYamlSync(filePath) {
|
|
82
|
+
if (!fs.existsSync(filePath))
|
|
83
|
+
return null;
|
|
84
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
85
|
+
return yaml_1.default.parse(content) || null;
|
|
86
|
+
}
|
|
87
|
+
function deepMerge(base, override) {
|
|
88
|
+
const result = { ...base };
|
|
89
|
+
for (const [key, value] of Object.entries(override)) {
|
|
90
|
+
if (value && typeof value === 'object' && !Array.isArray(value) &&
|
|
91
|
+
result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
|
|
92
|
+
result[key] = deepMerge(result[key], value);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
result[key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=specs-test-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specs-test-data.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-test-data.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
|
|
5
|
+
export class TestDataLoader {
|
|
6
|
+
private data: Record<string, any>;
|
|
7
|
+
|
|
8
|
+
private constructor(data: Record<string, any>) {
|
|
9
|
+
this.data = data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load test data for a screen/feature combination.
|
|
14
|
+
*
|
|
15
|
+
* Priority (later wins):
|
|
16
|
+
* 1. {feature}.yaml — base data
|
|
17
|
+
* 2. {feature}.{SUNGEN_ENV}.yaml — environment-specific (if SUNGEN_ENV set)
|
|
18
|
+
*/
|
|
19
|
+
static load(screenName: string, featureName: string): TestDataLoader {
|
|
20
|
+
const baseDir = path.join(process.cwd(), 'qa', 'screens', screenName, 'test-data');
|
|
21
|
+
const env = process.env.SUNGEN_ENV;
|
|
22
|
+
|
|
23
|
+
let data = loadYamlSync(path.join(baseDir, `${featureName}.yaml`)) || {};
|
|
24
|
+
|
|
25
|
+
if (env) {
|
|
26
|
+
const envData = loadYamlSync(path.join(baseDir, `${featureName}.${env}.yaml`));
|
|
27
|
+
if (envData) data = deepMerge(data, envData);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new TestDataLoader(data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get(key: string): string {
|
|
34
|
+
const parts = key.split('.');
|
|
35
|
+
let current: any = this.data;
|
|
36
|
+
for (const part of parts) {
|
|
37
|
+
if (current == null || typeof current !== 'object') {
|
|
38
|
+
throw new Error(`Test data key not found: ${key} (failed at '${part}')`);
|
|
39
|
+
}
|
|
40
|
+
current = current[part];
|
|
41
|
+
}
|
|
42
|
+
if (current === undefined || current === null) {
|
|
43
|
+
throw new Error(`Test data key not found: ${key}`);
|
|
44
|
+
}
|
|
45
|
+
return String(current);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadYamlSync(filePath: string): Record<string, any> | null {
|
|
50
|
+
if (!fs.existsSync(filePath)) return null;
|
|
51
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
52
|
+
return yaml.parse(content) || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function deepMerge(base: Record<string, any>, override: Record<string, any>): Record<string, any> {
|
|
56
|
+
const result = { ...base };
|
|
57
|
+
for (const [key, value] of Object.entries(override)) {
|
|
58
|
+
if (value && typeof value === 'object' && !Array.isArray(value) &&
|
|
59
|
+
result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
|
|
60
|
+
result[key] = deepMerge(result[key], value);
|
|
61
|
+
} else {
|
|
62
|
+
result[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|