@sun-asterisk/sungen 3.1.0 → 3.1.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/dist/cli/commands/challenge.d.ts.map +1 -1
- package/dist/cli/commands/challenge.js +9 -2
- package/dist/cli/commands/challenge.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +3 -2
- 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 +8 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +92 -76
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +3 -1
- package/dist/exporters/spec-parser.js.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/playwright/templates/scenario.hbs +19 -1
- package/dist/generators/test-generator/code-generator.d.ts +8 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +107 -3
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/database-patterns.d.ts +2 -1
- package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/database-patterns.js +2 -1
- package/dist/generators/test-generator/patterns/database-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +3 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/expect-patterns.js +54 -0
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +2 -0
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +6 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +8 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +3 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +5 -5
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/harness/challenge.d.ts +1 -0
- package/dist/harness/challenge.d.ts.map +1 -1
- package/dist/harness/challenge.js +49 -2
- package/dist/harness/challenge.js.map +1 -1
- package/dist/harness/data-driven-lint.d.ts +7 -0
- package/dist/harness/data-driven-lint.d.ts.map +1 -0
- package/dist/harness/data-driven-lint.js +153 -0
- package/dist/harness/data-driven-lint.js.map +1 -0
- package/dist/harness/parse.d.ts +2 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +16 -0
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/query-catalog.d.ts +48 -0
- package/dist/harness/query-catalog.d.ts.map +1 -0
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +7 -4
- package/dist/harness/script-check.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +40 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +40 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
- package/dist/orchestrator/templates/specs-db.d.ts +8 -0
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +22 -0
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +22 -0
- package/dist/orchestrator/templates/specs-test-data.ts +76 -15
- package/package.json +1 -1
- package/src/cli/commands/challenge.ts +6 -2
- package/src/cli/commands/delivery.ts +3 -2
- package/src/cli/commands/generate.ts +8 -0
- package/src/exporters/csv-exporter.ts +22 -6
- package/src/exporters/spec-parser.ts +3 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
- package/src/generators/test-generator/code-generator.ts +105 -3
- package/src/generators/test-generator/patterns/database-patterns.ts +2 -1
- package/src/generators/test-generator/patterns/expect-patterns.ts +49 -0
- package/src/generators/test-generator/patterns/index.ts +2 -0
- package/src/generators/test-generator/step-mapper.ts +9 -0
- package/src/generators/test-generator/template-engine.ts +3 -0
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +5 -5
- package/src/harness/challenge.ts +47 -2
- package/src/harness/data-driven-lint.ts +119 -0
- package/src/harness/parse.ts +12 -0
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/script-check.ts +8 -5
- package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +40 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +40 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
- package/src/orchestrator/templates/specs-db.ts +22 -0
- package/src/orchestrator/templates/specs-test-data.ts +76 -15
|
@@ -102,6 +102,22 @@ User see [Table] table match data:
|
|
|
102
102
|
|
|
103
103
|
Row scope: `see [Ref] row in [Table] table with {{v}}` enters scope. Subsequent `see [Col] column with {{v}}` checks cell in that row. Use `table match data:` for multi-row verification.
|
|
104
104
|
|
|
105
|
+
### Database verification (optional Data Driver)
|
|
106
|
+
|
|
107
|
+
Read-only DB-state checks. **Prefer named queries** — SQL lives in `qa/screens/<screen>/database/queries.yaml` (reviewed once, parameterized). Invoke with the `@query:<name>` annotation; it binds the result rows to `{{name}}`, then assert with `expect`:
|
|
108
|
+
|
|
109
|
+
```gherkin
|
|
110
|
+
@query:active_user # precondition: run query, bind {{active_user}}
|
|
111
|
+
@query:orders(buyer={{email}}) # …with explicit param override
|
|
112
|
+
Scenario: ...
|
|
113
|
+
Then expect {{active_user.count}} is at least {{one}} # ≥1 row
|
|
114
|
+
And expect {{active_user.first.status}} is "active" # first row's column
|
|
115
|
+
And expect {{orders.count}} is {{expected}} # exact count
|
|
116
|
+
And User see [Total] text is {{orders.first.total}} # UI ↔ DB
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Path access on a bound result: `{{q.count}}`/`{{q.length}}`, `{{q.first.col}}`, `{{q.last.col}}`, `{{q[2].col}}`, `{{q.col}}` (= first row's col). `expect A is B` also supports `is at least` / `is at most` / `is not`. Tier-2 declarative (trivial inline, no catalog): `User see [<table>] row where [<col>] is {{v}} [has [<col2>] = "x"]`, `… no row where …`, `… count is {{n}}`. Full grammar + catalog/datasource/secret rules → **Advanced → Database** doc. Only emit DB steps when the project has a `database/` catalog / `datasources.yaml`.
|
|
120
|
+
|
|
105
121
|
### States
|
|
106
122
|
|
|
107
123
|
`hidden` `visible` `disabled` `enabled` `checked` `unchecked` `focused` `empty` `loading` `selected` `sorted ascending` `sorted descending`
|
|
@@ -195,6 +211,30 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
|
|
|
195
211
|
| `@afterEach` | Hook: runs after each test → `test.afterEach()` (custom cleanup) |
|
|
196
212
|
| `@afterAll` | Hook: runs once after all tests → `test.afterAll()` |
|
|
197
213
|
| `@flow` | Mark feature as E2E flow (cross-screen testing) |
|
|
214
|
+
| `@cases:dataset` | Data-driven: run the scenario once per row of the `dataset` LIST in test-data → one `test()` per row |
|
|
215
|
+
| `@query:name` | Database: run the named query from `database/queries.yaml` (precondition) and bind its rows to `{{name}}`; assert with `expect {{name.count}} …` + path access. Override params `@query:name(p={{v}})`. Repeatable. (Optional Data Driver — see Database verification above) |
|
|
216
|
+
|
|
217
|
+
### Data-driven scenarios (`@cases`)
|
|
218
|
+
|
|
219
|
+
For one test case × many inputs (email/format/boundary validation, decision tables), tag the
|
|
220
|
+
scenario `@cases:<dataset>` and reference each row's columns as `{{col}}`. Put the rows as a LIST
|
|
221
|
+
in test-data — NOT inline; data stays runtime + env-overlayable.
|
|
222
|
+
|
|
223
|
+
```gherkin
|
|
224
|
+
@high @cases:email_validation
|
|
225
|
+
Scenario: VP-VAL-001 The email field rejects invalid formats
|
|
226
|
+
When User fill [Email] field with {{email}}
|
|
227
|
+
Then User see [Login Error] message with {{expected_error}}
|
|
228
|
+
```
|
|
229
|
+
```yaml
|
|
230
|
+
# test-data/<screen>.yaml
|
|
231
|
+
email_validation:
|
|
232
|
+
- { case: "no @", email: "plainaddress", expected_error: "Invalid email" }
|
|
233
|
+
- { case: "valid", email: "ok@x.com", expected_error: "" }
|
|
234
|
+
```
|
|
235
|
+
An optional `case`/`name`/`label` column labels each run. Each row → its own pass/fail. Prefer
|
|
236
|
+
`@cases` over duplicating a scenario per value. (Gherkin `Scenario Outline`/`Examples` is NOT
|
|
237
|
+
supported — use `@cases`.)
|
|
198
238
|
|
|
199
239
|
### Pass-through tags (filter at runtime via Playwright --grep)
|
|
200
240
|
|
|
@@ -54,6 +54,25 @@ user-invocable: false
|
|
|
54
54
|
OR condition: generate 1 scenario per branch where that branch alone triggers the outcome.
|
|
55
55
|
→ Happy-path only = missing the most common multi-condition implementation bug.
|
|
56
56
|
|
|
57
|
+
- **Many inputs, same steps → ONE data-driven scenario (`@cases`), not N copies:**
|
|
58
|
+
When a rule needs lots of inputs with the *same* step shape (email/format validation,
|
|
59
|
+
BVA boundary triples, EP classes, decision-table rows), tag one scenario `@cases:<dataset>`,
|
|
60
|
+
reference each row's columns as `{{col}}`, and put the rows as a LIST in test-data:
|
|
61
|
+
```gherkin
|
|
62
|
+
@high @cases:email_validation
|
|
63
|
+
Scenario: VP-VAL-001 The email field rejects invalid formats
|
|
64
|
+
When User fill [Email] field with {{email}}
|
|
65
|
+
Then User see [Error] message with {{expected_error}}
|
|
66
|
+
```
|
|
67
|
+
```yaml
|
|
68
|
+
email_validation:
|
|
69
|
+
- { case: "no @", email: "plainaddress", expected_error: "Invalid email" }
|
|
70
|
+
- { case: "valid", email: "ok@x.com", expected_error: "" }
|
|
71
|
+
```
|
|
72
|
+
→ one `test()` per row, each labelled by `case`. Adding inputs = editing test-data (no recompile),
|
|
73
|
+
and env overlays apply. Prefer this over duplicating a scenario per value. (Gherkin
|
|
74
|
+
`Scenario Outline`/`Examples` is NOT supported — use `@cases`.)
|
|
75
|
+
|
|
57
76
|
---
|
|
58
77
|
|
|
59
78
|
## Tier System
|
|
@@ -120,6 +120,7 @@ Build a mapping table: for each applicable group, does the feature have a matchi
|
|
|
120
120
|
- **EP**: keep only **one representative** per invalid class; same-class duplicates → flag as redundant.
|
|
121
121
|
- **BVA**: spec defines min/max → cover `min-1`, `min`, `max`, `max+1` (Maxlength, counts…).
|
|
122
122
|
- Error messages must match the spec **word-for-word**, not generic.
|
|
123
|
+
- **Data-driven (`@cases`)**: a `@cases:<dataset>` scenario legitimately covers many inputs in ONE scenario (one row per EP class / boundary / rule). Do **not** flag it as "too few negative cases" or as duplication — instead review the **dataset rows**: are all EP classes / boundary triples present, each labelled, expected values exact? N near-identical scenarios that differ only by input value → flag and recommend collapsing to `@cases`.
|
|
123
124
|
|
|
124
125
|
---
|
|
125
126
|
|
|
@@ -17,6 +17,12 @@ Apply selectively — not every screen needs all four techniques. Use the techni
|
|
|
17
17
|
|
|
18
18
|
**Rule:** These techniques determine **how many** and **which** scenarios to generate. `sungen-viewpoint` determines **which viewpoints** to cover.
|
|
19
19
|
|
|
20
|
+
**Implementing the data table → `@cases` (data-driven):** when EP classes / BVA boundary triples /
|
|
21
|
+
decision-table rows share the *same step shape* and differ only by input/expected values, encode
|
|
22
|
+
them as ONE `@cases:<dataset>` scenario (each class/boundary/rule = one row in the test-data list,
|
|
23
|
+
labelled by a `case` column) instead of N near-duplicate scenarios. The technique still decides the
|
|
24
|
+
rows; `@cases` is how you write them compactly. See `sungen-gherkin-syntax` → Data-driven.
|
|
25
|
+
|
|
20
26
|
---
|
|
21
27
|
|
|
22
28
|
## 1. Equivalence Partitioning (EP)
|
|
@@ -102,6 +102,22 @@ User see [Table] table match data:
|
|
|
102
102
|
|
|
103
103
|
Row scope: `see [Ref] row in [Table] table with {{v}}` enters scope. Subsequent `see [Col] column with {{v}}` checks cell in that row. Use `table match data:` for multi-row verification.
|
|
104
104
|
|
|
105
|
+
### Database verification (optional Data Driver)
|
|
106
|
+
|
|
107
|
+
Read-only DB-state checks. **Prefer named queries** — SQL lives in `qa/screens/<screen>/database/queries.yaml` (reviewed once, parameterized). Invoke with the `@query:<name>` annotation; it binds the result rows to `{{name}}`, then assert with `expect`:
|
|
108
|
+
|
|
109
|
+
```gherkin
|
|
110
|
+
@query:active_user # precondition: run query, bind {{active_user}}
|
|
111
|
+
@query:orders(buyer={{email}}) # …with explicit param override
|
|
112
|
+
Scenario: ...
|
|
113
|
+
Then expect {{active_user.count}} is at least {{one}} # ≥1 row
|
|
114
|
+
And expect {{active_user.first.status}} is "active" # first row's column
|
|
115
|
+
And expect {{orders.count}} is {{expected}} # exact count
|
|
116
|
+
And User see [Total] text is {{orders.first.total}} # UI ↔ DB
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Path access on a bound result: `{{q.count}}`/`{{q.length}}`, `{{q.first.col}}`, `{{q.last.col}}`, `{{q[2].col}}`, `{{q.col}}` (= first row's col). `expect A is B` also supports `is at least` / `is at most` / `is not`. Tier-2 declarative (trivial inline, no catalog): `User see [<table>] row where [<col>] is {{v}} [has [<col2>] = "x"]`, `… no row where …`, `… count is {{n}}`. Full grammar + catalog/datasource/secret rules → **Advanced → Database** doc. Only emit DB steps when the project has a `database/` catalog / `datasources.yaml`.
|
|
120
|
+
|
|
105
121
|
### States
|
|
106
122
|
|
|
107
123
|
`hidden` `visible` `disabled` `enabled` `checked` `unchecked` `focused` `empty` `loading` `selected` `sorted ascending` `sorted descending`
|
|
@@ -195,6 +211,30 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
|
|
|
195
211
|
| `@afterEach` | Hook: runs after each test → `test.afterEach()` (custom cleanup) |
|
|
196
212
|
| `@afterAll` | Hook: runs once after all tests → `test.afterAll()` |
|
|
197
213
|
| `@flow` | Mark feature as E2E flow (cross-screen testing) |
|
|
214
|
+
| `@cases:dataset` | Data-driven: run the scenario once per row of the `dataset` LIST in test-data → one `test()` per row |
|
|
215
|
+
| `@query:name` | Database: run the named query from `database/queries.yaml` (precondition) and bind its rows to `{{name}}`; assert with `expect {{name.count}} …` + path access. Override params `@query:name(p={{v}})`. Repeatable. (Optional Data Driver — see Database verification above) |
|
|
216
|
+
|
|
217
|
+
### Data-driven scenarios (`@cases`)
|
|
218
|
+
|
|
219
|
+
For one test case × many inputs (email/format/boundary validation, decision tables), tag the
|
|
220
|
+
scenario `@cases:<dataset>` and reference each row's columns as `{{col}}`. Put the rows as a LIST
|
|
221
|
+
in test-data — NOT inline; data stays runtime + env-overlayable.
|
|
222
|
+
|
|
223
|
+
```gherkin
|
|
224
|
+
@high @cases:email_validation
|
|
225
|
+
Scenario: VP-VAL-001 The email field rejects invalid formats
|
|
226
|
+
When User fill [Email] field with {{email}}
|
|
227
|
+
Then User see [Login Error] message with {{expected_error}}
|
|
228
|
+
```
|
|
229
|
+
```yaml
|
|
230
|
+
# test-data/<screen>.yaml
|
|
231
|
+
email_validation:
|
|
232
|
+
- { case: "no @", email: "plainaddress", expected_error: "Invalid email" }
|
|
233
|
+
- { case: "valid", email: "ok@x.com", expected_error: "" }
|
|
234
|
+
```
|
|
235
|
+
An optional `case`/`name`/`label` column labels each run. Each row → its own pass/fail. Prefer
|
|
236
|
+
`@cases` over duplicating a scenario per value. (Gherkin `Scenario Outline`/`Examples` is NOT
|
|
237
|
+
supported — use `@cases`.)
|
|
198
238
|
|
|
199
239
|
### Pass-through tags (filter at runtime via Playwright --grep)
|
|
200
240
|
|
|
@@ -54,6 +54,25 @@ user-invocable: false
|
|
|
54
54
|
OR condition: generate 1 scenario per branch where that branch alone triggers the outcome.
|
|
55
55
|
→ Happy-path only = missing the most common multi-condition implementation bug.
|
|
56
56
|
|
|
57
|
+
- **Many inputs, same steps → ONE data-driven scenario (`@cases`), not N copies:**
|
|
58
|
+
When a rule needs lots of inputs with the *same* step shape (email/format validation,
|
|
59
|
+
BVA boundary triples, EP classes, decision-table rows), tag one scenario `@cases:<dataset>`,
|
|
60
|
+
reference each row's columns as `{{col}}`, and put the rows as a LIST in test-data:
|
|
61
|
+
```gherkin
|
|
62
|
+
@high @cases:email_validation
|
|
63
|
+
Scenario: VP-VAL-001 The email field rejects invalid formats
|
|
64
|
+
When User fill [Email] field with {{email}}
|
|
65
|
+
Then User see [Error] message with {{expected_error}}
|
|
66
|
+
```
|
|
67
|
+
```yaml
|
|
68
|
+
email_validation:
|
|
69
|
+
- { case: "no @", email: "plainaddress", expected_error: "Invalid email" }
|
|
70
|
+
- { case: "valid", email: "ok@x.com", expected_error: "" }
|
|
71
|
+
```
|
|
72
|
+
→ one `test()` per row, each labelled by `case`. Adding inputs = editing test-data (no recompile),
|
|
73
|
+
and env overlays apply. Prefer this over duplicating a scenario per value. (Gherkin
|
|
74
|
+
`Scenario Outline`/`Examples` is NOT supported — use `@cases`.)
|
|
75
|
+
|
|
57
76
|
---
|
|
58
77
|
|
|
59
78
|
## Tier System
|
|
@@ -120,6 +120,7 @@ Build a mapping table: for each applicable group, does the feature have a matchi
|
|
|
120
120
|
- **EP**: keep only **one representative** per invalid class; same-class duplicates → flag as redundant.
|
|
121
121
|
- **BVA**: spec defines min/max → cover `min-1`, `min`, `max`, `max+1` (Maxlength, counts…).
|
|
122
122
|
- Error messages must match the spec **word-for-word**, not generic.
|
|
123
|
+
- **Data-driven (`@cases`)**: a `@cases:<dataset>` scenario legitimately covers many inputs in ONE scenario (one row per EP class / boundary / rule). Do **not** flag it as "too few negative cases" or as duplication — instead review the **dataset rows**: are all EP classes / boundary triples present, each labelled, expected values exact? N near-identical scenarios that differ only by input value → flag and recommend collapsing to `@cases`.
|
|
123
124
|
|
|
124
125
|
---
|
|
125
126
|
|
package/src/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md
CHANGED
|
@@ -17,6 +17,12 @@ Apply selectively — not every screen needs all four techniques. Use the techni
|
|
|
17
17
|
|
|
18
18
|
**Rule:** These techniques determine **how many** and **which** scenarios to generate. `sungen-viewpoint` determines **which viewpoints** to cover.
|
|
19
19
|
|
|
20
|
+
**Implementing the data table → `@cases` (data-driven):** when EP classes / BVA boundary triples /
|
|
21
|
+
decision-table rows share the *same step shape* and differ only by input/expected values, encode
|
|
22
|
+
them as ONE `@cases:<dataset>` scenario (each class/boundary/rule = one row in the test-data list,
|
|
23
|
+
labelled by a `case` column) instead of N near-duplicate scenarios. The technique still decides the
|
|
24
|
+
rows; `@cases` is how you write them compactly. See `sungen-gherkin-syntax` → Data-driven.
|
|
25
|
+
|
|
20
26
|
---
|
|
21
27
|
|
|
22
28
|
## 1. Equivalence Partitioning (EP)
|
|
@@ -138,6 +138,28 @@ class DataSource {
|
|
|
138
138
|
private sqlFor(conf: DataSourceConfig, sql: string): string {
|
|
139
139
|
return conf.engine === 'sqlite' ? sql.replace(/\$\d+/g, '?') : sql;
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
// --- Named queries (catalog-backed; SQL is resolved + embedded at compile time) -----------
|
|
143
|
+
/** Read-only guard (second layer): a named query must be a single SELECT/WITH statement. */
|
|
144
|
+
private assertSelectOnly(label: string, sql: string): void {
|
|
145
|
+
const s = sql.trim().replace(/;\s*$/, '');
|
|
146
|
+
if (!/^(SELECT|WITH)\b/i.test(s)) throw new Error(`Data Driver: ${label} is not a read-only SELECT — refused.`);
|
|
147
|
+
if (s.includes(';')) throw new Error(`Data Driver: ${label} contains multiple statements — refused.`);
|
|
148
|
+
if (/\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|MERGE|REPLACE|CALL|EXEC|EXECUTE|ATTACH|PRAGMA|VACUUM)\b/i.test(s)) {
|
|
149
|
+
throw new Error(`Data Driver: ${label} contains a write/DDL keyword — refused.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Run a catalog query (read-only) and return its rows. The result is bound to a `{{name}}`
|
|
155
|
+
* variable via `testData.bind(...)`, so the scenario asserts on it with `expect …` steps and
|
|
156
|
+
* path access (`{{name.count}}`, `{{name.first.col}}`, `{{name[2].col}}`).
|
|
157
|
+
*/
|
|
158
|
+
async fetchQuery(label: string, sql: string, params: any[], datasource?: string): Promise<any[]> {
|
|
159
|
+
this.assertSelectOnly(label, sql);
|
|
160
|
+
const { engine, conf } = await this.engine(datasource);
|
|
161
|
+
return engine.query(this.sqlFor(conf, sql), params);
|
|
162
|
+
}
|
|
141
163
|
}
|
|
142
164
|
|
|
143
165
|
function desc(filter: Record<string, any>): string {
|
|
@@ -5,6 +5,8 @@ import yaml from 'yaml';
|
|
|
5
5
|
|
|
6
6
|
export class TestDataLoader {
|
|
7
7
|
private data: Record<string, any>;
|
|
8
|
+
// Data-driven (@cases): when set (via withRow), get() prefers this row's columns.
|
|
9
|
+
private row?: Record<string, any>;
|
|
8
10
|
|
|
9
11
|
private constructor(data: Record<string, any>) {
|
|
10
12
|
this.data = data;
|
|
@@ -41,23 +43,56 @@ export class TestDataLoader {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
get(key: string): string {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let current: any = this.data[key];
|
|
47
|
-
if (current === undefined || current === null) {
|
|
48
|
-
// Fall back to nested navigation for YAML-structured keys (e.g. "cart.qty_two").
|
|
49
|
-
current = this.data;
|
|
50
|
-
for (const part of key.split('.')) {
|
|
51
|
-
if (current == null || typeof current !== 'object') {
|
|
52
|
-
throw new Error(`Test data key not found: ${key} (failed at '${part}')`);
|
|
53
|
-
}
|
|
54
|
-
current = current[part];
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (current === undefined || current === null) {
|
|
46
|
+
const value = this.resolve(key);
|
|
47
|
+
if (value === undefined || value === null) {
|
|
58
48
|
throw new Error(`Test data key not found: ${key}`);
|
|
59
49
|
}
|
|
60
|
-
return String(
|
|
50
|
+
return String(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a `{{...}}` reference to its raw value. Supports:
|
|
55
|
+
* - flat keys (incl. captured runtime vars stored under a literal dotted key);
|
|
56
|
+
* - `@cases` row columns (the current row wins);
|
|
57
|
+
* - structured paths over nested data AND `@query`-bound result arrays:
|
|
58
|
+
* `q.count` / `q.length` → number of rows
|
|
59
|
+
* `q.first.col` / `q.last.col` / `q[2].col` → a specific row's column
|
|
60
|
+
* `q.col` → shorthand for the first row's column
|
|
61
|
+
*/
|
|
62
|
+
private resolve(key: string): any {
|
|
63
|
+
// 1. Exact flat key — captured vars (set()) live under a literal, possibly dotted, key.
|
|
64
|
+
if (this.row && key in this.row && this.row[key] !== undefined && this.row[key] !== null) {
|
|
65
|
+
return this.row[key];
|
|
66
|
+
}
|
|
67
|
+
if (this.data[key] !== undefined && this.data[key] !== null) {
|
|
68
|
+
return this.data[key];
|
|
69
|
+
}
|
|
70
|
+
// 2. Structured path: head from the row (cases) or shared data, then walk segments.
|
|
71
|
+
const tokens = String(key).replace(/\[(\d+)\]/g, '.$1').split('.');
|
|
72
|
+
let cur: any = (this.row && tokens[0] in this.row) ? this.row[tokens[0]] : this.data[tokens[0]];
|
|
73
|
+
for (let i = 1; i < tokens.length && cur != null; i++) cur = TestDataLoader.step(cur, tokens[i]);
|
|
74
|
+
return cur;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One navigation step over an array (with count/first/last/index/field-shorthand) or object. */
|
|
78
|
+
private static step(cur: any, token: string): any {
|
|
79
|
+
if (Array.isArray(cur)) {
|
|
80
|
+
if (token === 'count' || token === 'length') return cur.length;
|
|
81
|
+
if (token === 'first') return cur[0];
|
|
82
|
+
if (token === 'last') return cur[cur.length - 1];
|
|
83
|
+
if (/^\d+$/.test(token)) return cur[Number(token)];
|
|
84
|
+
return cur[0] == null ? undefined : cur[0][token]; // shorthand: first row's field
|
|
85
|
+
}
|
|
86
|
+
if (cur && typeof cur === 'object') return cur[token];
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Bind a raw value (e.g. an `@query` result array) under `key` so `{{key.…}}` paths resolve.
|
|
92
|
+
* Unlike set(), the value is stored as-is (array/object), not coerced to a string.
|
|
93
|
+
*/
|
|
94
|
+
bind(key: string, value: any): void {
|
|
95
|
+
this.data[key] = value;
|
|
61
96
|
}
|
|
62
97
|
|
|
63
98
|
/**
|
|
@@ -68,6 +103,32 @@ export class TestDataLoader {
|
|
|
68
103
|
set(key: string, value: string): void {
|
|
69
104
|
this.data[key] = value;
|
|
70
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Data-driven (@cases): return the list of rows at `key` (after env-overlay merge),
|
|
109
|
+
* each stamped with `__label` for the test title / report. Throws if missing or not a list.
|
|
110
|
+
*/
|
|
111
|
+
cases(key: string): Array<Record<string, any>> {
|
|
112
|
+
const list = this.data[key];
|
|
113
|
+
if (!Array.isArray(list)) {
|
|
114
|
+
throw new Error(`@cases dataset "${key}" not found or not a list in test-data (got ${typeof list}).`);
|
|
115
|
+
}
|
|
116
|
+
return list.map((row: any, i: number) => {
|
|
117
|
+
const r: Record<string, any> = (row && typeof row === 'object' && !Array.isArray(row)) ? { ...row } : { value: row };
|
|
118
|
+
r.__label = String(r.case ?? r.name ?? r.label ?? `row ${i + 1}`);
|
|
119
|
+
return r;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Data-driven (@cases): a view whose get() prefers the given row's columns and falls
|
|
125
|
+
* back to the shared data. Used inside the per-row test() loop.
|
|
126
|
+
*/
|
|
127
|
+
withRow(row: Record<string, any>): TestDataLoader {
|
|
128
|
+
const view = new TestDataLoader({ ...this.data }); // clone → per-row set() stays isolated
|
|
129
|
+
view.row = row;
|
|
130
|
+
return view;
|
|
131
|
+
}
|
|
71
132
|
}
|
|
72
133
|
|
|
73
134
|
function loadYamlSync(filePath: string): Record<string, any> | null {
|