@step-forge/step-forge 0.0.7-beta.6 → 0.0.7

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.
Files changed (69) hide show
  1. package/.claude/settings.local.json +17 -0
  2. package/.eslintignore +6 -0
  3. package/.eslintrc +18 -0
  4. package/.prettierignore +6 -0
  5. package/.prettierrc +15 -0
  6. package/CHANGELOG.md +28 -0
  7. package/CLAUDE.md +115 -0
  8. package/README.md +45 -39
  9. package/cucumber.mjs +39 -0
  10. package/dist/step-forge.cjs +1 -1
  11. package/dist/step-forge.js +143 -120
  12. package/dist/types/src/builderTypeUtils.d.ts +14 -0
  13. package/dist/types/src/common.d.ts +32 -0
  14. package/dist/types/src/given.d.ts +73 -0
  15. package/dist/types/src/index.d.ts +5 -0
  16. package/dist/types/src/then.d.ts +73 -0
  17. package/dist/types/src/utils.d.ts +5 -0
  18. package/dist/types/src/when.d.ts +72 -0
  19. package/dist/types/src/world.d.ts +21 -0
  20. package/docs/assets/state_deps.gif +0 -0
  21. package/dts-bundle-generator.config.cjs +19 -0
  22. package/features/TESTING.md +100 -0
  23. package/features/analyzer/analyzer.feature +83 -0
  24. package/features/analyzer/fixtures/ambiguous-step.feature +5 -0
  25. package/features/analyzer/fixtures/missing-given-dep.feature +5 -0
  26. package/features/analyzer/fixtures/missing-multiple-deps.feature +6 -0
  27. package/features/analyzer/fixtures/missing-when-dep.feature +5 -0
  28. package/features/analyzer/fixtures/steps.ts +113 -0
  29. package/features/analyzer/fixtures/undefined-step.feature +5 -0
  30. package/features/analyzer/fixtures/undefined-with-missing-dep.feature +5 -0
  31. package/features/analyzer/fixtures/valid-and-but-keywords.feature +8 -0
  32. package/features/analyzer/fixtures/valid-deps.feature +6 -0
  33. package/features/analyzer/fixtures/valid-no-deps.feature +6 -0
  34. package/features/analyzer/fixtures/valid-optional-deps.feature +6 -0
  35. package/features/analyzer/fixtures/valid-variables.feature +6 -0
  36. package/features/basic.feature +21 -0
  37. package/features/exported.feature +6 -0
  38. package/features/steps/analyzerSteps.ts +53 -0
  39. package/features/steps/commonSteps.ts +93 -0
  40. package/features/steps/exportedSteps.ts +42 -0
  41. package/features/steps/world.ts +29 -0
  42. package/package.json +33 -26
  43. package/rolldown.config.mjs +34 -0
  44. package/src/analyzer/cli.ts +96 -0
  45. package/src/analyzer/gherkinParser.ts +175 -0
  46. package/src/analyzer/index.ts +77 -0
  47. package/src/analyzer/rules/ambiguousStepRule.ts +36 -0
  48. package/src/analyzer/rules/dependencyRule.ts +59 -0
  49. package/src/analyzer/rules/index.ts +23 -0
  50. package/src/analyzer/rules/undefinedStepRule.ts +31 -0
  51. package/src/analyzer/stepExtractor.ts +432 -0
  52. package/src/analyzer/stepMatcher.ts +67 -0
  53. package/src/analyzer/types.ts +55 -0
  54. package/src/builderTypeUtils.ts +27 -0
  55. package/src/common.ts +138 -0
  56. package/src/given.ts +102 -0
  57. package/src/index.ts +6 -0
  58. package/src/then.ts +128 -0
  59. package/src/utils.ts +74 -0
  60. package/src/when.ts +118 -0
  61. package/src/world.ts +90 -0
  62. package/test/givenCompilationTests.ts +130 -0
  63. package/test/testUtils.ts +30 -0
  64. package/test/thenCompilationTests.ts +207 -0
  65. package/test/whenCompilationTests.ts +157 -0
  66. package/ts-node-esm-register.js +8 -0
  67. package/tsconfig.cucumber.json +14 -0
  68. package/tsconfig.json +29 -0
  69. package/dist/types/index.d.ts +0 -301
@@ -0,0 +1,19 @@
1
+ const config = {
2
+ compilationOptions: {
3
+ preferredConfigPath: "./tsconfig.json",
4
+ },
5
+ entries: [
6
+ {
7
+ filePath: "./src/index.ts",
8
+ outFile: "./build/dist/index.d.ts",
9
+ noCheck: false,
10
+ },
11
+ {
12
+ filePath: "./src/analyzer/index.ts",
13
+ outFile: "./build/dist/analyzer.d.ts",
14
+ noCheck: false,
15
+ },
16
+ ],
17
+ };
18
+
19
+ module.exports = config;
@@ -0,0 +1,100 @@
1
+ # Testing
2
+
3
+ All tests use Cucumber.js in a self-testing pattern: feature files in `features/` with step definitions in `features/steps/` exercise the library.
4
+
5
+ ## Test Scripts
6
+
7
+ | Script | Profile | Description |
8
+ | ----------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |
9
+ | `npm test` | `all` | Run all tests. Suppresses stderr for clean output. Use for normal development. |
10
+ | `npm run test:debug` | `all` | Run all tests with full output (stderr included). Use when a test fails and you need stack traces or error details. |
11
+ | `npm run test:cucumber` | `default` | Same paths as `all`, but does not set `PORT` or `LOG_LEVEL`. |
12
+ | `npm run test:ci` | `ci` | CI-oriented profile. Does not import `src/**/*.ts` (only step defs). Enables `publish`. |
13
+
14
+ All profiles run with `parallel: 1` and use `tsx` as the TypeScript loader.
15
+
16
+ There is no way to run a single test file. To run a subset, use Cucumber tags or modify the feature files temporarily.
17
+
18
+ ## Directory Structure
19
+
20
+ ```
21
+ features/
22
+ basic.feature ← Builder pattern tests (variables, deps, state)
23
+ exported.feature ← Re-exported builder tests
24
+ steps/
25
+ commonSteps.ts ← Step defs for basic.feature
26
+ exportedSteps.ts ← Step defs for exported.feature
27
+ analyzerSteps.ts ← Step defs for analyzer tests
28
+ world.ts ← World state types
29
+ analyzer/
30
+ analyzer.feature ← Analyzer test scenarios (run by Cucumber)
31
+ fixtures/
32
+ steps.ts ← Fixture step definitions (data, NOT run by Cucumber)
33
+ valid-no-deps.feature ← Fixture feature files (data, NOT run by Cucumber)
34
+ valid-deps.feature
35
+ ...
36
+ ```
37
+
38
+ ## Builder Tests
39
+
40
+ `basic.feature` and `exported.feature` test the core builder pattern — step registration, variable extraction, dependency resolution, and state merging. Their step definitions in `features/steps/` use the builders directly and assert on the resulting world state.
41
+
42
+ Type-safety tests in `test/` use `@ts-expect-error` annotations and validate at `tsc` compile time, not at runtime.
43
+
44
+ ## Analyzer Tests
45
+
46
+ The analyzer test suite under `features/analyzer/` tests the `analyze()` API against fixture files. The key distinction: **fixture files are data, not tests**. The analyzer's extractor reads `fixtures/steps.ts` as a TypeScript AST, and the parser reads `fixtures/*.feature` as Gherkin data. Cucumber never executes them.
47
+
48
+ The `cucumber.mjs` config uses non-recursive path globs (`./features/*.feature`, `./features/analyzer/*.feature`) so fixture files in `features/analyzer/fixtures/` are excluded from Cucumber's test discovery.
49
+
50
+ ### Available Steps
51
+
52
+ Step definitions in `features/steps/analyzerSteps.ts` provide:
53
+
54
+ | Step | Purpose |
55
+ | ----------------------------------------- | ------------------------------------------------------ |
56
+ | `Given step definitions from {string}` | Set the fixture step file (relative to `fixtures/`) |
57
+ | `Given a feature file {string}` | Set the fixture feature file (relative to `fixtures/`) |
58
+ | `When I analyze the files` | Call `analyze()` with the configured files |
59
+ | `Then there should be no errors` | Assert zero errors in diagnostics |
60
+ | `Then there should be {int} error/errors` | Assert exact error count |
61
+ | `Then an error should mention {string}` | Assert an error message contains the substring |
62
+
63
+ ### Adding New Analyzer Tests
64
+
65
+ 1. **Add step definition patterns** (if needed) to `features/analyzer/fixtures/steps.ts`. This file must be valid TypeScript that compiles, but it is never executed — only parsed by the AST extractor.
66
+
67
+ 2. **Create a fixture feature file** in `features/analyzer/fixtures/` using the step expressions from `steps.ts`. This file is parsed by the Gherkin parser as data.
68
+
69
+ 3. **Add a scenario** to `features/analyzer/analyzer.feature`:
70
+
71
+ ```gherkin
72
+ Scenario: Description of what you're testing
73
+ Given a feature file "your-new-fixture.feature"
74
+ When I analyze the files
75
+ Then there should be no errors
76
+ ```
77
+
78
+ The Background already provides `Given step definitions from "steps.ts"`, so you only need the `Given a feature file` line in each scenario.
79
+
80
+ 4. **Run `npm run test:debug`** to verify. Use `test:debug` instead of `npm test` so you can see error details if something fails.
81
+
82
+ ### Fixture Step Definitions
83
+
84
+ `features/analyzer/fixtures/steps.ts` contains builder calls covering these patterns:
85
+
86
+ | Expression | Type | Dependencies | Produces |
87
+ | ----------------------------- | ----- | ------------------------- | --------- |
88
+ | `a user` | given | none | `user` |
89
+ | `a user named {string}` | given | none | `user` |
90
+ | `an account` | given | none | `account` |
91
+ | `I started` | given | none | (nothing) |
92
+ | `I save the user` | when | `given.user: required` | `user` |
93
+ | `I delete the account` | when | `given.account: required` | `result` |
94
+ | `I got here` | when | none | (nothing) |
95
+ | `everything was good` | then | none | (nothing) |
96
+ | `there is a user` | then | `when.user: required` | (nothing) |
97
+ | `the user's name is {string}` | then | `when.user: required` | (nothing) |
98
+ | `the account might exist` | then | `given.account: optional` | (nothing) |
99
+
100
+ When adding new patterns, add them to this file and they'll be available to all fixture feature files.
@@ -0,0 +1,83 @@
1
+ Feature: Analyzer dependency verification
2
+
3
+ Background:
4
+ Given step definitions from "steps.ts"
5
+
6
+ # --- Passing scenarios ---
7
+
8
+ Scenario: No dependencies produces no errors
9
+ Given a feature file "valid-no-deps.feature"
10
+ When I analyze the files
11
+ Then there should be no errors
12
+
13
+ Scenario: Satisfied required dependencies produce no errors
14
+ Given a feature file "valid-deps.feature"
15
+ When I analyze the files
16
+ Then there should be no errors
17
+
18
+ Scenario: Variables with satisfied dependencies produce no errors
19
+ Given a feature file "valid-variables.feature"
20
+ When I analyze the files
21
+ Then there should be no errors
22
+
23
+ Scenario: Optional dependencies not produced produce no errors
24
+ Given a feature file "valid-optional-deps.feature"
25
+ When I analyze the files
26
+ Then there should be no errors
27
+
28
+ Scenario: And/But keywords resolve correctly with satisfied deps
29
+ Given a feature file "valid-and-but-keywords.feature"
30
+ When I analyze the files
31
+ Then there should be no errors
32
+
33
+ # --- Undefined step scenarios ---
34
+
35
+ Scenario: Undefined step reports an error
36
+ Given a feature file "undefined-step.feature"
37
+ When I analyze the files
38
+ Then there should be 1 error
39
+ And there is 1 error for rule "undefined-step"
40
+ And an error should mention "I do something that does not exist"
41
+ And an error should mention "does not match any step definition"
42
+
43
+ Scenario: Undefined step combined with missing dependency
44
+ Given a feature file "undefined-with-missing-dep.feature"
45
+ When I analyze the files
46
+ Then there should be 2 errors
47
+ And there is 1 error for rule "undefined-step"
48
+ And an error should mention "I do something that does not exist"
49
+ And there is 1 error for rule "dependency-check"
50
+ And an error should mention "given.user"
51
+
52
+ # --- Ambiguous step scenarios ---
53
+
54
+ Scenario: Ambiguous step reports an error
55
+ Given a feature file "ambiguous-step.feature"
56
+ When I analyze the files
57
+ Then there should be 1 error
58
+ And there is 1 error for rule "ambiguous-step"
59
+ And an error should mention "matches multiple step definitions"
60
+
61
+ # --- Failing scenarios ---
62
+
63
+ Scenario: Missing required given dependency reports an error
64
+ Given a feature file "missing-given-dep.feature"
65
+ When I analyze the files
66
+ Then there should be 1 error
67
+ And there is 1 error for rule "dependency-check"
68
+ And an error should mention "given.user"
69
+
70
+ Scenario: Missing required when dependency reports an error
71
+ Given a feature file "missing-when-dep.feature"
72
+ When I analyze the files
73
+ Then there should be 1 error
74
+ And there is 1 error for rule "dependency-check"
75
+ And an error should mention "when.user"
76
+
77
+ Scenario: Multiple missing dependencies reports multiple errors
78
+ Given a feature file "missing-multiple-deps.feature"
79
+ When I analyze the files
80
+ Then there should be 2 errors
81
+ And there are 2 errors for rule "dependency-check"
82
+ And an error should mention "given.user"
83
+ And an error should mention "given.account"
@@ -0,0 +1,5 @@
1
+ Feature: Ambiguous step
2
+ Scenario: Uses an ambiguous step
3
+ Given the system is ready
4
+ When I got here
5
+ Then everything was good
@@ -0,0 +1,5 @@
1
+ Feature: Missing given dependency
2
+
3
+ Scenario: When step needs given.user but nothing produces it
4
+ When I save the user
5
+ Then everything was good
@@ -0,0 +1,6 @@
1
+ Feature: Multiple missing dependencies
2
+
3
+ Scenario: Multiple steps with missing deps
4
+ When I save the user
5
+ When I delete the account
6
+ Then there is a user
@@ -0,0 +1,5 @@
1
+ Feature: Missing when dependency
2
+
3
+ Scenario: Then step needs when.user but nothing produces it
4
+ Given a user
5
+ Then there is a user
@@ -0,0 +1,113 @@
1
+ import { givenBuilder } from "../../../src/given";
2
+ import { whenBuilder } from "../../../src/when";
3
+ import { thenBuilder } from "../../../src/then";
4
+
5
+ type GivenState = {
6
+ user: { type: string; token: string };
7
+ account: { id: string };
8
+ };
9
+ type WhenState = {
10
+ user: { type: string; token: string; saved: boolean };
11
+ result: { success: boolean };
12
+ };
13
+ type ThenState = Record<string, never>;
14
+
15
+ // --- No dependency steps --- //
16
+
17
+ givenBuilder<GivenState>()
18
+ .statement("I started")
19
+ .step(() => ({}))
20
+ .register();
21
+
22
+ whenBuilder<GivenState, WhenState>()
23
+ .statement("I got here")
24
+ .step(() => ({}))
25
+ .register();
26
+
27
+ thenBuilder<GivenState, WhenState, ThenState>()
28
+ .statement("everything was good")
29
+ .step(() => {})
30
+ .register();
31
+
32
+ // --- Steps that produce state --- //
33
+
34
+ givenBuilder<GivenState>()
35
+ .statement("a user")
36
+ .step(() => {
37
+ return {
38
+ user: { type: "person", token: "abc" },
39
+ };
40
+ })
41
+ .register();
42
+
43
+ givenBuilder<GivenState>()
44
+ .statement((name: string) => `a user named ${name}`)
45
+ .step(({ variables: [name] }) => {
46
+ return {
47
+ user: { type: "person", token: name },
48
+ };
49
+ })
50
+ .register();
51
+
52
+ givenBuilder<GivenState>()
53
+ .statement("an account")
54
+ .step(() => {
55
+ return {
56
+ account: { id: "acct-1" },
57
+ };
58
+ })
59
+ .register();
60
+
61
+ // --- Ambiguous steps (same expression, two definitions) --- //
62
+
63
+ givenBuilder<GivenState>()
64
+ .statement("the system is ready")
65
+ .step(() => ({}))
66
+ .register();
67
+
68
+ givenBuilder<GivenState>()
69
+ .statement("the system is ready")
70
+ .step(() => ({}))
71
+ .register();
72
+
73
+ // --- When steps with dependencies --- //
74
+
75
+ whenBuilder<GivenState, WhenState>()
76
+ .statement("I save the user")
77
+ .dependencies({ given: { user: "required" } })
78
+ .step(({ given: { user } }) => {
79
+ return {
80
+ user: { ...user, saved: true },
81
+ };
82
+ })
83
+ .register();
84
+
85
+ whenBuilder<GivenState, WhenState>()
86
+ .statement("I delete the account")
87
+ .dependencies({ given: { account: "required" } })
88
+ .step(() => {
89
+ return {
90
+ result: { success: true },
91
+ };
92
+ })
93
+ .register();
94
+
95
+ // --- Then steps with dependencies --- //
96
+
97
+ thenBuilder<GivenState, WhenState, ThenState>()
98
+ .statement("there is a user")
99
+ .dependencies({ when: { user: "required" } })
100
+ .step(() => {})
101
+ .register();
102
+
103
+ thenBuilder<GivenState, WhenState, ThenState>()
104
+ .statement((name: string) => `the user's name is ${name}`)
105
+ .dependencies({ when: { user: "required" } })
106
+ .step(() => {})
107
+ .register();
108
+
109
+ thenBuilder<GivenState, WhenState, ThenState>()
110
+ .statement("the account might exist")
111
+ .dependencies({ given: { account: "optional" } })
112
+ .step(() => {})
113
+ .register();
@@ -0,0 +1,5 @@
1
+ Feature: Undefined step
2
+ Scenario: Uses an undefined step
3
+ Given a user
4
+ When I do something that does not exist
5
+ Then everything was good
@@ -0,0 +1,5 @@
1
+ Feature: Undefined step with missing dependency
2
+ Scenario: Has both undefined and missing dependency
3
+ When I save the user
4
+ When I do something that does not exist
5
+ Then everything was good
@@ -0,0 +1,8 @@
1
+ Feature: And/But keyword resolution
2
+
3
+ Scenario: And and But resolve correctly
4
+ Given a user
5
+ And an account
6
+ When I save the user
7
+ But I delete the account
8
+ Then there is a user
@@ -0,0 +1,6 @@
1
+ Feature: Satisfied dependencies
2
+
3
+ Scenario: Full chain with required deps
4
+ Given a user
5
+ When I save the user
6
+ Then there is a user
@@ -0,0 +1,6 @@
1
+ Feature: No dependencies
2
+
3
+ Scenario: Steps with no dependencies
4
+ Given I started
5
+ When I got here
6
+ Then everything was good
@@ -0,0 +1,6 @@
1
+ Feature: Optional dependencies
2
+
3
+ Scenario: Optional dep not produced is fine
4
+ Given I started
5
+ When I got here
6
+ Then the account might exist
@@ -0,0 +1,6 @@
1
+ Feature: Variables with dependencies
2
+
3
+ Scenario: Variables and satisfied deps
4
+ Given a user named "John"
5
+ When I save the user
6
+ Then the user's name is "John"
@@ -0,0 +1,21 @@
1
+ Feature: Steps written in gherkin can be matched to Step Forge steps
2
+
3
+ Scenario: Basic feature steps with no dependencies or variables can be run as a cucumber scenario
4
+ Given I started
5
+ When I got here
6
+ Then everything was good
7
+
8
+ Scenario: Basic feature steps with dependencies can be run as a cucumber scenario
9
+ Given a user
10
+ When I save the user
11
+ Then there is a user
12
+
13
+ Scenario: Basic feature steps with variables can be run as a cucumber scenario
14
+ Given a user named "John"
15
+ When I save the user
16
+ Then there is a user
17
+
18
+ Scenario: Basic feature steps with dependencies and variables can be run as a cucumber scenario
19
+ Given a user
20
+ When I name the user "John"
21
+ Then the user's name is "John"
@@ -0,0 +1,6 @@
1
+ Feature: Builders can be re-exported for ease of use
2
+
3
+ Scenario: Exported steps can be used in cucumber scenarios
4
+ Given a bank user
5
+ When I deposit "1000" "USD"
6
+ Then the balance is "1000"
@@ -0,0 +1,53 @@
1
+ import * as path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { Given, When, Then } from "@cucumber/cucumber";
4
+ import { expect } from "earl";
5
+ import { analyze, Diagnostic } from "../../src/analyzer/index";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const fixturesDir = path.resolve(__dirname, "../analyzer/fixtures");
9
+
10
+ let fixtureStepFile: string;
11
+ let fixtureFeatureFile: string;
12
+ let diagnostics: Diagnostic[];
13
+
14
+ Given("step definitions from {string}", function (fileName: string) {
15
+ fixtureStepFile = path.join(fixturesDir, fileName);
16
+ });
17
+
18
+ Given("a feature file {string}", function (fileName: string) {
19
+ fixtureFeatureFile = path.join(fixturesDir, fileName);
20
+ });
21
+
22
+ When("I analyze the files", async function () {
23
+ diagnostics = await analyze({
24
+ stepFiles: [fixtureStepFile],
25
+ featureFiles: [fixtureFeatureFile],
26
+ });
27
+ });
28
+
29
+ Then("there should be no errors", function () {
30
+ const errors = diagnostics.filter((d) => d.severity === "error");
31
+ expect(errors).toHaveLength(0);
32
+ });
33
+
34
+ Then("there should be {int} error/errors", function (count: number) {
35
+ const errors = diagnostics.filter((d) => d.severity === "error");
36
+ expect(errors).toHaveLength(count);
37
+ });
38
+
39
+ Then("an error should mention {string}", function (substring: string) {
40
+ const errors = diagnostics.filter((d) => d.severity === "error");
41
+ const found = errors.some((e) => e.message.includes(substring));
42
+ expect(found).toEqual(true);
43
+ });
44
+
45
+ Then(
46
+ "there is/are {int} error/errors for rule {string}",
47
+ function (count: number, rule: string) {
48
+ const errors = diagnostics.filter(
49
+ (d) => d.severity === "error" && d.rule === rule
50
+ );
51
+ expect(errors).toHaveLength(count);
52
+ }
53
+ );
@@ -0,0 +1,93 @@
1
+ import { givenBuilder } from "../../src/given";
2
+ import { whenBuilder } from "../../src/when";
3
+ import { thenBuilder } from "../../src/then";
4
+ import { GivenState, ThenState, WhenState } from "./world";
5
+ import { expect } from "earl";
6
+
7
+ // --- No dependency no variable steps --- //
8
+ givenBuilder<GivenState>()
9
+ .statement("I started")
10
+ .step(() => ({}))
11
+ .register();
12
+
13
+ whenBuilder<GivenState, WhenState>()
14
+ .statement("I got here")
15
+ .step(() => ({}))
16
+ .register();
17
+
18
+ thenBuilder<GivenState, WhenState, ThenState>()
19
+ .statement("everything was good")
20
+ .step(() => ({}))
21
+ .register();
22
+
23
+ // --- Dependency only steps --- //
24
+ givenBuilder<GivenState>()
25
+ .statement("a user")
26
+ .step(() => {
27
+ return {
28
+ user: {
29
+ type: "person",
30
+ token: "random",
31
+ },
32
+ };
33
+ })
34
+ .register();
35
+
36
+ whenBuilder<GivenState, WhenState>()
37
+ .statement("I save the user")
38
+ .dependencies({ given: { user: "required" } })
39
+ .step(({ given: { user } }) => {
40
+ return {
41
+ user: {
42
+ ...user,
43
+ saved: true,
44
+ },
45
+ };
46
+ })
47
+ .register();
48
+
49
+ thenBuilder<GivenState, WhenState, ThenState>()
50
+ .statement("there is a user")
51
+ .dependencies({ when: { user: "required" } })
52
+ .step(({ when: { user } }) => {
53
+ expect(user.saved).toBeTruthy();
54
+ })
55
+ .register();
56
+
57
+ // --- Variable only steps --- //
58
+ givenBuilder<GivenState>()
59
+ .statement((userName: string) => `a user named ${userName}`)
60
+ .step(({ variables: [userName] }) => {
61
+ return {
62
+ user: {
63
+ type: "person",
64
+ token: userName,
65
+ },
66
+ };
67
+ })
68
+ .register();
69
+
70
+ // --- More complex steps --- //
71
+
72
+ whenBuilder<GivenState, WhenState>()
73
+ .statement((userName: string) => `I name the user ${userName}`)
74
+ .dependencies({ given: { user: "required" } })
75
+ .step(({ given: { user }, variables: [userName] }) => {
76
+ return {
77
+ user: {
78
+ ...user,
79
+ token: userName,
80
+ saved: true,
81
+ },
82
+ };
83
+ })
84
+ .register();
85
+
86
+ thenBuilder<GivenState, WhenState, ThenState>()
87
+ .statement((userName: string) => `the user's name is ${userName}`)
88
+ .dependencies({ when: { user: "required" } })
89
+ .step(({ when: { user }, variables: [userName] }) => {
90
+ const token = user.token;
91
+ expect(token).toEqual(userName);
92
+ })
93
+ .register();
@@ -0,0 +1,42 @@
1
+ import { expect } from "earl";
2
+ import { givenBuilder } from "../../src/given";
3
+ import { thenBuilder } from "../../src/then";
4
+ import { whenBuilder } from "../../src/when";
5
+ import { GivenState, ThenState, WhenState } from "./world";
6
+
7
+ const Given = givenBuilder<GivenState>().statement;
8
+ const When = whenBuilder<GivenState, WhenState>().statement;
9
+ const Then = thenBuilder<GivenState, WhenState, ThenState>().statement;
10
+
11
+ Given("a bank user")
12
+ .step(() => {
13
+ return {
14
+ user: {
15
+ type: "customer",
16
+ token: "random",
17
+ },
18
+ };
19
+ })
20
+ .register();
21
+
22
+ When((amount: string, currency: string) => `I deposit ${amount} ${currency}`)
23
+ .dependencies({ given: { user: "required" } })
24
+ .step(({ variables: [rawAmount, currency], given: { user } }) => {
25
+ const amount = parseFloat(rawAmount);
26
+ return {
27
+ deposit: {
28
+ amount,
29
+ currency,
30
+ user,
31
+ },
32
+ };
33
+ })
34
+ .register();
35
+
36
+ Then((amount: string) => `the balance is ${amount}`)
37
+ .dependencies({ when: { deposit: "required" } })
38
+ .step(({ when: { deposit }, variables: [rawAmount] }) => {
39
+ const amount = parseFloat(rawAmount);
40
+ expect(deposit.amount).toEqual(amount);
41
+ })
42
+ .register();
@@ -0,0 +1,29 @@
1
+ import { setWorldConstructor } from "@cucumber/cucumber";
2
+ import { BasicWorld } from "../../src/world";
3
+
4
+ export interface GivenState {
5
+ user: {
6
+ type: string;
7
+ token: string;
8
+ };
9
+ }
10
+
11
+ export interface WhenState {
12
+ user: {
13
+ type: string;
14
+ token: string;
15
+ saved: boolean;
16
+ };
17
+ deposit: {
18
+ amount: number;
19
+ currency: string;
20
+ user: {
21
+ type: string;
22
+ token: string;
23
+ };
24
+ };
25
+ }
26
+
27
+ export interface ThenState {}
28
+
29
+ setWorldConstructor(BasicWorld<GivenState, WhenState, ThenState>);