bdd-vitest 0.1.0
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/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-R4KYBEVR.js +107 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +84 -0
- package/dist/levels.d.ts +136 -0
- package/dist/levels.js +13 -0
- package/dist/mock-ai.d.ts +67 -0
- package/dist/mock-ai.js +83 -0
- package/dist/mock-fetch.d.ts +37 -0
- package/dist/mock-fetch.js +51 -0
- package/dist/mock-server.d.ts +46 -0
- package/dist/mock-server.js +77 -0
- package/dist/preset.d.ts +6 -0
- package/dist/preset.js +27 -0
- package/dist/process.d.ts +38 -0
- package/dist/process.js +75 -0
- package/dist/service.d.ts +110 -0
- package/dist/service.js +249 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mattias Wetterlind
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# bdd-vitest
|
|
2
|
+
|
|
3
|
+
Enforced Given/When/Then for Vitest. Tests become documentation. ~200 lines. Zero config.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { unit, feature, expect } from "bdd-vitest";
|
|
7
|
+
|
|
8
|
+
feature("Checkout", () => {
|
|
9
|
+
unit("applies discount over 500kr", {
|
|
10
|
+
given: ["a cart with total 600kr", () => createCart(600)],
|
|
11
|
+
when: ["checking out", (cart) => checkout(cart)],
|
|
12
|
+
then: ["10% discount applied", (res) => expect(res.discount).toBe(60)],
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Read just the descriptions - you understand the system without opening production code.
|
|
18
|
+
|
|
19
|
+
## Why
|
|
20
|
+
|
|
21
|
+
Most test frameworks let you write `it("does something", () => {})` with no structure inside. AI agents skip descriptions. Tired humans skip assertions. Tests pass but prove nothing.
|
|
22
|
+
|
|
23
|
+
bdd-vitest makes it impossible:
|
|
24
|
+
|
|
25
|
+
- **Descriptions are required.** Every phase is a `["description", fn]` tuple. TypeScript rejects missing descriptions at compile time.
|
|
26
|
+
- **Levels are required.** No generic `scenario` - you must pick `unit`, `component`, `integration`, or `e2e`. Each has enforced timeouts.
|
|
27
|
+
- **Assertions are required.** `then` is mandatory. No test without a check.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -D bdd-vitest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Levels
|
|
36
|
+
|
|
37
|
+
Every test must declare its level. Wrong level => timeout fails the test. Slow for its level => warning nudges you.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { unit, component, integration, e2e } from "bdd-vitest";
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Level | Timeout | What belongs here |
|
|
44
|
+
|-------|---------|-------------------|
|
|
45
|
+
| `unit` | 100ms | Pure functions, calculations, parsing, validation |
|
|
46
|
+
| `component` | 5s | One service with mocked deps (mockServer, mockFetch) |
|
|
47
|
+
| `integration` | 30s | Multiple real services talking to each other |
|
|
48
|
+
| `e2e` | 120s | Full system, browser, real network, real database |
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
⚠️ [unit] "parse config" took 80ms (warn: 50ms, limit: 100ms). Is this a component test?
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Know it's intentionally slow? `slow: true` suppresses the warning (timeout still enforced).
|
|
55
|
+
|
|
56
|
+
## Phases
|
|
57
|
+
|
|
58
|
+
`then` is always required. Everything else is optional:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
unit("full", { given, when, then }); // setup => action => assert
|
|
62
|
+
unit("no action", { given, then }); // setup => assert
|
|
63
|
+
unit("no setup", { when, then }); // action => assert
|
|
64
|
+
unit("assertion", { then }); // just assert
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
No setup code? `given` can be just a description:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
component("health check", {
|
|
71
|
+
given: "a running server",
|
|
72
|
+
when: ["requesting /health", () => app.request("/health")],
|
|
73
|
+
then: ["returns 200", (res) => expect(res.status).toBe(200)],
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Context flows through - `when` receives `given`'s return, `then` receives both:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
unit("FIFO order", {
|
|
81
|
+
given: ["a queue with tracker", () => ({ order: [] as number[] })],
|
|
82
|
+
when: ["processing tasks", async (ctx) => {
|
|
83
|
+
await enqueueAll([1, 2, 3], (n) => { ctx.order.push(n); });
|
|
84
|
+
return ctx.order;
|
|
85
|
+
}],
|
|
86
|
+
then: ["order preserved", (result) => expect(result).toEqual([1, 2, 3])],
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Errors show which phase failed:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
AssertionError: [given] Database connection failed
|
|
94
|
+
AssertionError: [when] Request timeout
|
|
95
|
+
AssertionError: [then] expected 42 to be 43
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Mock server
|
|
99
|
+
|
|
100
|
+
No dependencies. Real HTTP server on a random port:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { mockServer } from "bdd-vitest/mock-server";
|
|
104
|
+
|
|
105
|
+
component("retries on 503", {
|
|
106
|
+
given: ["an unreliable API", mockServer({
|
|
107
|
+
"POST /v1/completions": [
|
|
108
|
+
{ status: 503, body: { error: "overloaded" } },
|
|
109
|
+
{ status: 200, body: { result: "ok" } },
|
|
110
|
+
],
|
|
111
|
+
})],
|
|
112
|
+
when: ["sending with retry", (server) => fetchWithRetry(`${server.url}/v1/completions`)],
|
|
113
|
+
then: ["succeeds", (res) => expect(res.status).toBe(200)],
|
|
114
|
+
cleanup: (server) => server.close(),
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Response shortcuts:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
"GET /users": { name: "Alice" } // => 200 + JSON
|
|
122
|
+
"DELETE /users/1": 204 // => status only
|
|
123
|
+
"POST /submit": [{ status: 503 }, { status: 200, body: { ok: true } }] // => sequential
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Mock fetch
|
|
127
|
+
|
|
128
|
+
Same idea, patches global `fetch` instead of starting a server:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { mockFetch } from "bdd-vitest/mock-fetch";
|
|
132
|
+
|
|
133
|
+
component("handles 404", {
|
|
134
|
+
given: ["github returns 404", mockFetch({ "GET https://api.github.com/users/x": 404 })],
|
|
135
|
+
when: ["fetching user", () => fetch("https://api.github.com/users/x")],
|
|
136
|
+
then: ["returns 404", (res) => expect(res.status).toBe(404)],
|
|
137
|
+
cleanup: (mock) => mock.restore(),
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Table-driven
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
unit.outline("adds numbers", [
|
|
145
|
+
{ name: "positives", a: 2, b: 3, expected: 5 },
|
|
146
|
+
{ name: "negatives", a: -1, b: 1, expected: 0 },
|
|
147
|
+
], {
|
|
148
|
+
given: (row) => ({ a: row.a as number, b: row.b as number }),
|
|
149
|
+
when: (ctx) => ctx.a + ctx.b,
|
|
150
|
+
then: (result, _ctx, row) => expect(result).toBe(row.expected),
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Grouping
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
feature("Auth", () => {
|
|
158
|
+
rule("valid credentials", () => {
|
|
159
|
+
unit("grants access", { ... });
|
|
160
|
+
});
|
|
161
|
+
rule("expired tokens", () => {
|
|
162
|
+
component("refreshes automatically", { ... });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## API
|
|
168
|
+
|
|
169
|
+
| Export | What |
|
|
170
|
+
|--------|------|
|
|
171
|
+
| `unit` / `component` / `integration` / `e2e` | Test with enforced level + timeout |
|
|
172
|
+
| `.skip` / `.only` / `.group` / `.outline` | Modifiers on any level |
|
|
173
|
+
| `feature(name, fn)` / `rule(name, fn)` | Grouping (describe aliases) |
|
|
174
|
+
| `mockServer(routes)` | Declarative HTTP mock server |
|
|
175
|
+
| `mockFetch(routes)` | Patches global fetch |
|
|
176
|
+
| `expect` | Re-exported from vitest |
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__require
|
|
10
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/levels.ts
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
var LEVELS = {
|
|
4
|
+
unit: { timeout: 100, warnAt: 50, name: "unit", nextLevel: "component" },
|
|
5
|
+
component: { timeout: 5e3, warnAt: 2e3, name: "component", nextLevel: "integration" },
|
|
6
|
+
integration: { timeout: 3e4, warnAt: 15e3, name: "integration", nextLevel: "e2e" },
|
|
7
|
+
e2e: { timeout: 12e4, warnAt: 6e4, name: "e2e" }
|
|
8
|
+
};
|
|
9
|
+
function createLevelRunner(level) {
|
|
10
|
+
function run(name, phases) {
|
|
11
|
+
it(name, { timeout: level.timeout }, async () => {
|
|
12
|
+
const start = performance.now();
|
|
13
|
+
let phase = "given";
|
|
14
|
+
let context = void 0;
|
|
15
|
+
try {
|
|
16
|
+
if (phases.given && typeof phases.given !== "string") {
|
|
17
|
+
context = await phases.given[1]();
|
|
18
|
+
}
|
|
19
|
+
phase = "when";
|
|
20
|
+
const result = phases.when ? await phases.when[1](context) : context;
|
|
21
|
+
phase = "then";
|
|
22
|
+
await phases.then[1](result, context);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error instanceof Error && !error.message.startsWith("[")) {
|
|
25
|
+
error.message = `[${phase}] ${error.message}`;
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
} finally {
|
|
29
|
+
if (phases.cleanup) {
|
|
30
|
+
await phases.cleanup(context);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const elapsed = performance.now() - start;
|
|
34
|
+
const warnAt = level.warnAt ?? level.timeout * 0.5;
|
|
35
|
+
if (!phases.slow && elapsed > warnAt) {
|
|
36
|
+
const next = level.nextLevel ? ` Is this a ${level.nextLevel} test?` : "";
|
|
37
|
+
console.warn(
|
|
38
|
+
`\u26A0\uFE0F [${level.name}] "${name}" took ${Math.round(elapsed)}ms (warn: ${warnAt}ms, limit: ${level.timeout}ms).${next}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
run.skip = function(name, _phases) {
|
|
44
|
+
it.skip(name, () => {
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
run.only = function(name, phases) {
|
|
48
|
+
it.only(name, { timeout: level.timeout }, async () => {
|
|
49
|
+
let phase = "given";
|
|
50
|
+
let context = void 0;
|
|
51
|
+
try {
|
|
52
|
+
if (phases.given && typeof phases.given !== "string") {
|
|
53
|
+
context = await phases.given[1]();
|
|
54
|
+
}
|
|
55
|
+
phase = "when";
|
|
56
|
+
const result = phases.when ? await phases.when[1](context) : context;
|
|
57
|
+
phase = "then";
|
|
58
|
+
await phases.then[1](result, context);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof Error && !error.message.startsWith("[")) {
|
|
61
|
+
error.message = `[${phase}] ${error.message}`;
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
} finally {
|
|
65
|
+
if (phases.cleanup) {
|
|
66
|
+
await phases.cleanup(context);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
return run;
|
|
72
|
+
}
|
|
73
|
+
function createLevelGroup(level) {
|
|
74
|
+
return function group(name, fn) {
|
|
75
|
+
describe(`[${level.name}] ${name}`, fn);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function createLevelOutline(level) {
|
|
79
|
+
return function(name, table, phases) {
|
|
80
|
+
describe(`[${level.name}] ${name}`, () => {
|
|
81
|
+
for (const row of table) {
|
|
82
|
+
it(row.name, { timeout: level.timeout }, async () => {
|
|
83
|
+
const context = await phases.given(row);
|
|
84
|
+
const result = await phases.when(context, row);
|
|
85
|
+
await phases.then(result, context, row);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function buildLevel(config) {
|
|
92
|
+
const runner = createLevelRunner(config);
|
|
93
|
+
runner.group = createLevelGroup(config);
|
|
94
|
+
runner.outline = createLevelOutline(config);
|
|
95
|
+
return runner;
|
|
96
|
+
}
|
|
97
|
+
var unit = buildLevel(LEVELS.unit);
|
|
98
|
+
var component = buildLevel(LEVELS.component);
|
|
99
|
+
var integration = buildLevel(LEVELS.integration);
|
|
100
|
+
var e2e = buildLevel(LEVELS.e2e);
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
unit,
|
|
104
|
+
component,
|
|
105
|
+
integration,
|
|
106
|
+
e2e
|
|
107
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
component,
|
|
3
|
+
e2e,
|
|
4
|
+
integration,
|
|
5
|
+
unit
|
|
6
|
+
} from "./chunk-R4KYBEVR.js";
|
|
7
|
+
import "./chunk-3RG5ZIWI.js";
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
import { describe, it } from "vitest";
|
|
11
|
+
import { expect as expect2 } from "vitest";
|
|
12
|
+
function createScenarioRunner(itFn) {
|
|
13
|
+
return function runScenario(name, phases) {
|
|
14
|
+
itFn(name, async () => {
|
|
15
|
+
let phase = "given";
|
|
16
|
+
try {
|
|
17
|
+
const context = phases.given ? typeof phases.given === "string" ? void 0 : await phases.given[1]() : void 0;
|
|
18
|
+
phase = "when";
|
|
19
|
+
const result = phases.when ? await phases.when[1](context) : context;
|
|
20
|
+
phase = "then";
|
|
21
|
+
await phases.then[1](result, context);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof Error && !error.message.startsWith("[")) {
|
|
24
|
+
error.message = `[${phase}] ${error.message}`;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
var _scenario = createScenarioRunner(it);
|
|
32
|
+
var _scenarioOnly = createScenarioRunner(it.only);
|
|
33
|
+
var _scenarioSkip = createScenarioRunner(it.skip);
|
|
34
|
+
function scenario(name, phases) {
|
|
35
|
+
_scenario(name, phases);
|
|
36
|
+
}
|
|
37
|
+
scenario.only = function(name, phases) {
|
|
38
|
+
_scenarioOnly(name, phases);
|
|
39
|
+
};
|
|
40
|
+
scenario.skip = function(name, phases) {
|
|
41
|
+
_scenarioSkip(name, phases);
|
|
42
|
+
};
|
|
43
|
+
function feature(name, fn) {
|
|
44
|
+
describe(name, fn);
|
|
45
|
+
}
|
|
46
|
+
function rule(name, fn) {
|
|
47
|
+
describe(name, fn);
|
|
48
|
+
}
|
|
49
|
+
function scenarioWithCleanup(name, phases) {
|
|
50
|
+
it(name, async () => {
|
|
51
|
+
const context = phases.given ? typeof phases.given === "string" ? void 0 : await phases.given[1]() : void 0;
|
|
52
|
+
try {
|
|
53
|
+
const result = phases.when ? await phases.when[1](context) : context;
|
|
54
|
+
await phases.then[1](result, context);
|
|
55
|
+
} finally {
|
|
56
|
+
if (phases.cleanup) {
|
|
57
|
+
await phases.cleanup(context);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function scenarioOutline(name, table, phases) {
|
|
63
|
+
describe(name, () => {
|
|
64
|
+
for (const row of table) {
|
|
65
|
+
it(row.name, async () => {
|
|
66
|
+
const context = await phases.given(row);
|
|
67
|
+
const result = await phases.when(context, row);
|
|
68
|
+
await phases.then(result, context, row);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
component,
|
|
75
|
+
e2e,
|
|
76
|
+
expect2 as expect,
|
|
77
|
+
feature,
|
|
78
|
+
integration,
|
|
79
|
+
rule,
|
|
80
|
+
scenario,
|
|
81
|
+
scenarioOutline,
|
|
82
|
+
scenarioWithCleanup,
|
|
83
|
+
unit
|
|
84
|
+
};
|
package/dist/levels.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test levels with enforced constraints.
|
|
5
|
+
*
|
|
6
|
+
* Each level has a timeout and rules about what's allowed.
|
|
7
|
+
* Break the rules → runtime error. No ambiguity.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { unit, component, integration } from "bdd-vitest/levels";
|
|
11
|
+
*
|
|
12
|
+
* unit("adds numbers", {
|
|
13
|
+
* given: ["two numbers", () => ({ a: 2, b: 3 })],
|
|
14
|
+
* when: ["adding", (ctx) => ctx.a + ctx.b],
|
|
15
|
+
* then: ["returns sum", (r) => expect(r).toBe(5)],
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface LevelConfig {
|
|
20
|
+
/** Max time per scenario (ms) */
|
|
21
|
+
timeout: number;
|
|
22
|
+
/** Warn if test takes longer than this (ms). Default: 50% of timeout. */
|
|
23
|
+
warnAt?: number;
|
|
24
|
+
/** Level name for error messages */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Suggested next level (for warning message) */
|
|
27
|
+
nextLevel?: string;
|
|
28
|
+
}
|
|
29
|
+
interface LevelScenario<TContext, TResult> {
|
|
30
|
+
given?: Phase<() => TContext | Promise<TContext>> | string;
|
|
31
|
+
when?: Phase<(context: TContext) => TResult | Promise<TResult>>;
|
|
32
|
+
then: Phase<(result: TResult, context: TContext) => void | Promise<void>>;
|
|
33
|
+
cleanup?: (context: TContext) => void | Promise<void>;
|
|
34
|
+
/** Suppress slow-test warning. Use when you know the test is intentionally slow for its level. */
|
|
35
|
+
slow?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface TableRow$1 {
|
|
38
|
+
name: string;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
interface LevelRunner {
|
|
42
|
+
<TContext, TResult>(name: string, phases: LevelScenario<TContext, TResult>): void;
|
|
43
|
+
skip: <TContext, TResult>(name: string, phases: LevelScenario<TContext, TResult>) => void;
|
|
44
|
+
only: <TContext, TResult>(name: string, phases: LevelScenario<TContext, TResult>) => void;
|
|
45
|
+
group: (name: string, fn: () => void) => void;
|
|
46
|
+
outline: <TRow extends TableRow$1, TContext, TResult>(name: string, table: TRow[], phases: {
|
|
47
|
+
given: (row: TRow) => TContext | Promise<TContext>;
|
|
48
|
+
when: (context: TContext, row: TRow) => TResult | Promise<TResult>;
|
|
49
|
+
then: (result: TResult, context: TContext, row: TRow) => void | Promise<void>;
|
|
50
|
+
}) => void;
|
|
51
|
+
}
|
|
52
|
+
/** Pure logic. No I/O, no mocks, no services. <100ms. */
|
|
53
|
+
declare const unit: LevelRunner;
|
|
54
|
+
/** Service in isolation. Mocked dependencies. <5s. */
|
|
55
|
+
declare const component: LevelRunner;
|
|
56
|
+
/** Multiple services together. Real dependencies. <30s. */
|
|
57
|
+
declare const integration: LevelRunner;
|
|
58
|
+
/** Full system, browser, network. <120s. */
|
|
59
|
+
declare const e2e: LevelRunner;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* bdd-vitest — Enforced Given/When/Then for Vitest
|
|
63
|
+
*
|
|
64
|
+
* Usage:
|
|
65
|
+
* import { feature, scenario } from "bdd-vitest";
|
|
66
|
+
*
|
|
67
|
+
* feature("Queue", () => {
|
|
68
|
+
* scenario("rejects when full", {
|
|
69
|
+
* given: () => createQueue({ maxSize: 50, filled: 50 }),
|
|
70
|
+
* when: (queue) => queue.enqueue(mockRequest()).catch(e => e),
|
|
71
|
+
* then: (error) => expect(error.message).toContain("Queue full"),
|
|
72
|
+
* });
|
|
73
|
+
* });
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/** Phase: [description, function] tuple — description is enforced */
|
|
77
|
+
type Phase<TFn> = [desc: string, fn: TFn];
|
|
78
|
+
interface Scenario<TContext, TResult> {
|
|
79
|
+
/** Setup — tuple, description-only string, or omitted */
|
|
80
|
+
given?: Phase<() => TContext | Promise<TContext>> | string;
|
|
81
|
+
/** Action — tuple or omitted */
|
|
82
|
+
when?: Phase<(context: TContext) => TResult | Promise<TResult>>;
|
|
83
|
+
/** Assertion — required */
|
|
84
|
+
then: Phase<(result: TResult, context: TContext) => void | Promise<void>>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated Use `unit`, `component`, `integration`, or `e2e` instead.
|
|
88
|
+
* Choosing a level is required — it enforces timeouts and communicates intent.
|
|
89
|
+
*/
|
|
90
|
+
declare function scenario<TContext, TResult>(name: string, phases: Scenario<TContext, TResult>): void;
|
|
91
|
+
declare namespace scenario {
|
|
92
|
+
var only: <TContext, TResult>(name: string, phases: Scenario<TContext, TResult>) => void;
|
|
93
|
+
var skip: <TContext, TResult>(name: string, phases: Scenario<TContext, TResult>) => void;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Groups related scenarios. Alias for describe with intent.
|
|
97
|
+
*/
|
|
98
|
+
declare function feature(name: string, fn: () => void): void;
|
|
99
|
+
/**
|
|
100
|
+
* Sub-groups within a feature for related business rules.
|
|
101
|
+
*/
|
|
102
|
+
declare function rule(name: string, fn: () => void): void;
|
|
103
|
+
interface ScenarioWithLifecycle<TContext, TResult> {
|
|
104
|
+
given?: Phase<() => TContext | Promise<TContext>> | string;
|
|
105
|
+
when?: Phase<(context: TContext) => TResult | Promise<TResult>>;
|
|
106
|
+
then: Phase<(result: TResult, context: TContext) => void | Promise<void>>;
|
|
107
|
+
cleanup?: (context: TContext) => void | Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Scenario with automatic cleanup after assertion.
|
|
111
|
+
*/
|
|
112
|
+
declare function scenarioWithCleanup<TContext, TResult>(name: string, phases: ScenarioWithLifecycle<TContext, TResult>): void;
|
|
113
|
+
interface TableRow {
|
|
114
|
+
name: string;
|
|
115
|
+
[key: string]: unknown;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run the same scenario with multiple data rows.
|
|
119
|
+
*
|
|
120
|
+
* Usage:
|
|
121
|
+
* scenarioOutline("adds numbers", [
|
|
122
|
+
* { name: "positive", a: 2, b: 3, expected: 5 },
|
|
123
|
+
* { name: "negative", a: -1, b: 1, expected: 0 },
|
|
124
|
+
* ], {
|
|
125
|
+
* given: (row) => ({ a: row.a as number, b: row.b as number }),
|
|
126
|
+
* when: (ctx) => ctx.a + ctx.b,
|
|
127
|
+
* then: (result, _ctx, row) => expect(result).toBe(row.expected),
|
|
128
|
+
* });
|
|
129
|
+
*/
|
|
130
|
+
declare function scenarioOutline<TRow extends TableRow, TContext, TResult>(name: string, table: TRow[], phases: {
|
|
131
|
+
given: (row: TRow) => TContext | Promise<TContext>;
|
|
132
|
+
when: (context: TContext, row: TRow) => TResult | Promise<TResult>;
|
|
133
|
+
then: (result: TResult, context: TContext, row: TRow) => void | Promise<void>;
|
|
134
|
+
}): void;
|
|
135
|
+
|
|
136
|
+
export { type LevelConfig, type LevelRunner, type LevelScenario, type Phase as P, type Scenario as S, type TableRow as T, type TableRow$1 as TableRow, type ScenarioWithLifecycle as a, scenarioOutline as b, scenarioWithCleanup as c, component, e2e, feature as f, integration, rule as r, scenario as s, unit };
|
package/dist/levels.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock AI provider for testing — never calls real APIs.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { createMockProvider, createMockAuthProfiles } from "bdd-vitest/mock-ai";
|
|
6
|
+
*/
|
|
7
|
+
interface MockProviderOptions {
|
|
8
|
+
/** Simulated response latency in ms (default: 10) */
|
|
9
|
+
latencyMs?: number;
|
|
10
|
+
/** Default response text */
|
|
11
|
+
response?: string;
|
|
12
|
+
/** Simulate failure after N requests */
|
|
13
|
+
failAfter?: number;
|
|
14
|
+
/** Error message on failure */
|
|
15
|
+
errorMessage?: string;
|
|
16
|
+
}
|
|
17
|
+
interface MockProviderStats {
|
|
18
|
+
totalRequests: number;
|
|
19
|
+
activeRequests: number;
|
|
20
|
+
maxConcurrent: number;
|
|
21
|
+
requestLog: Array<{
|
|
22
|
+
model: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
declare function createMockProvider(options?: MockProviderOptions): {
|
|
28
|
+
complete: (model: string, _messages: unknown[]) => Promise<{
|
|
29
|
+
id: string;
|
|
30
|
+
object: "chat.completion";
|
|
31
|
+
created: number;
|
|
32
|
+
model: string;
|
|
33
|
+
choices: {
|
|
34
|
+
index: number;
|
|
35
|
+
message: {
|
|
36
|
+
role: "assistant";
|
|
37
|
+
content: string;
|
|
38
|
+
};
|
|
39
|
+
finish_reason: "stop";
|
|
40
|
+
}[];
|
|
41
|
+
usage: {
|
|
42
|
+
prompt_tokens: number;
|
|
43
|
+
completion_tokens: number;
|
|
44
|
+
total_tokens: number;
|
|
45
|
+
};
|
|
46
|
+
}>;
|
|
47
|
+
stats: MockProviderStats;
|
|
48
|
+
reset: () => void;
|
|
49
|
+
};
|
|
50
|
+
interface MockAuthProfile {
|
|
51
|
+
type: string;
|
|
52
|
+
provider: string;
|
|
53
|
+
access: string;
|
|
54
|
+
refresh: string;
|
|
55
|
+
expires: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Creates a mock auth-profiles.json structure.
|
|
59
|
+
* Tokens are obviously fake — never hit real APIs.
|
|
60
|
+
*/
|
|
61
|
+
declare function createMockAuthProfiles(providers?: string[]): Record<string, MockAuthProfile>;
|
|
62
|
+
/**
|
|
63
|
+
* Creates expired mock credentials for testing refresh flows.
|
|
64
|
+
*/
|
|
65
|
+
declare function createExpiredMockAuthProfiles(providers?: string[]): Record<string, MockAuthProfile>;
|
|
66
|
+
|
|
67
|
+
export { type MockAuthProfile, type MockProviderOptions, type MockProviderStats, createExpiredMockAuthProfiles, createMockAuthProfiles, createMockProvider };
|