@shivaedev/effect-test 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +197 -73
- package/dist/eventually.d.ts.map +1 -1
- package/dist/eventually.js +28 -22
- package/dist/eventually.js.map +1 -1
- package/dist/types.d.ts +15 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/vitest.d.ts +1 -1
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +46 -37
- package/dist/vitest.js.map +1 -1
- package/docs/north-star.md +43 -0
- package/docs/roadmap.md +24 -0
- package/package.json +5 -4
- package/src/eventually.ts +9 -5
- package/src/types.ts +25 -18
- package/src/vitest.ts +38 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 - 2026-10-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `makeEffectIt` fixture testers accept Effect-returning bodies as well as generators, including table cases and tester variants. Both forms retain harness, service, clock and per-test Scope behavior.
|
|
8
|
+
- Export `EffectTestBody<THarness, TProvided, A>` from `types.ts` to describe both fixture body forms.
|
|
9
|
+
- `makeEffectIt` harnesses, wrappers, generator bodies and table bodies can use the per-test `Scope` already supplied by the runner. The worker Context and inferred Layer services stay unchanged; unrelated services still require a matching Layer.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Name descriptive generic type parameters with a `T` prefix, preserving test runner types and behavior.
|
|
14
|
+
- Verify conditional skipping in an isolated Vitest fixture, with passing suite assertions for registration and body non-execution instead of a skipped suite test.
|
|
15
|
+
- Explain the runner's purpose, service and test lifetimes, clock choices and typed readiness polling in the README, agent north star and package docs; publish `docs/` with the README.
|
|
16
|
+
- Build package JavaScript, declarations and source maps with TypeScript 7.
|
|
17
|
+
|
|
3
18
|
## 0.3.1 - 2026-10-06
|
|
4
19
|
|
|
5
20
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,109 +1,233 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @shivaedev/effect-test
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Each test gets a fresh Scope, a generator body, and TestClock unless you ask
|
|
5
|
-
for the live clock.
|
|
3
|
+
Effect tests should spend their lines on the behavior they check. This package gives ordinary tests and reusable fixtures one Vitest runner, so a generator can use the application's services and a controlled clock without repeating the runner around every test.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
`makeEffectIt`. Use this package directly when those wrappers are the wrong
|
|
9
|
-
shape.
|
|
5
|
+
## Why you want this
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
built for ShivaeDev applications and may change without a deprecation period.
|
|
7
|
+
Wrapping every body in `Effect.gen` hides the test's work, and copying a runner into each fixture makes service and clock setup drift. Pass the generator itself and read the test from top to bottom:
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
```ts
|
|
10
|
+
import { expect } from "@effect/vitest";
|
|
11
|
+
import { Clock } from "effect";
|
|
12
|
+
import * as TestClock from "effect/testing/TestClock";
|
|
13
|
+
import { it } from "@shivaedev/effect-test/it.ts";
|
|
14
|
+
|
|
15
|
+
it.effect("advances the test's time", function* () {
|
|
16
|
+
expect(yield* Clock.currentTimeMillis).toBe(0);
|
|
17
|
+
yield* TestClock.adjust("1 second");
|
|
18
|
+
expect(yield* Clock.currentTimeMillis).toBe(1000);
|
|
19
|
+
});
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
That is a whole test. The body starts at time zero and advances its clock by one second. For a suite that needs services, the same package makes a tester from a Layer and a harness, so the suite defines its plumbing once and each spec keeps its own work.
|
|
23
|
+
|
|
24
|
+
## Using it
|
|
25
|
+
|
|
26
|
+
### How to think about it
|
|
27
|
+
|
|
28
|
+
There are two ways to enter the runner. `it.effect` and `it.live` are the ordinary testers: their body receives Vitest's test context and uses services it provides inside its own Effect. `makeEffectIt` defines a tester for a reusable fixture: its body receives a harness first, then the test context, and can yield the services its Layer provides.
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
A **Layer** describes how to acquire the fixture's services. `makeEffectIt` builds it in a Vitest worker fixture and retains the resulting Effect **Context**, the map from service keys to service values. A **harness** is the value made for one test using those services. It can hold a service, a fixture identifier or the domain entry points the test needs. The **test context** is Vitest's context, with the test's name, assertion helpers and hooks; it is a different thing from Effect's Context.
|
|
31
|
+
|
|
32
|
+
Keep the two lifetimes separate. The worker owns the acquired Layer and its scope. Each test makes its harness and runs its body in a separate scope. Harnesses, wrappers, generator bodies and `each` bodies may acquire resources through that Scope; their finalizers run on success, failure and interruption. Scope is available per test and is not added to the worker Context. A shared service can retain mutable state across tests; a fresh harness alone does not reset it. Put application-state isolation in the fixture that owns that state, such as a database transaction wrapper.
|
|
33
|
+
|
|
34
|
+
The work usually splits into two parts:
|
|
35
|
+
|
|
36
|
+
1. Once per fixture, define its Layer, `makeHarness` and, when needed, an `around` wrapper in test support.
|
|
37
|
+
2. In every test, write the generator and the assertions. For a test with no reusable fixture, use `it.effect` directly.
|
|
38
|
+
|
|
39
|
+
### 1. Once per fixture: services and the harness
|
|
24
40
|
|
|
25
41
|
```ts
|
|
26
|
-
import {
|
|
42
|
+
import { expect } from "@effect/vitest";
|
|
43
|
+
import { Clock, Context, Effect, Layer } from "effect";
|
|
44
|
+
import { makeEffectIt } from "@shivaedev/effect-test/vitest.ts";
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
expect(yield* Clock.currentTimeMillis).toBe(0)
|
|
30
|
-
})
|
|
46
|
+
class Token extends Context.Service<Token, string>()("test/Token") {}
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
const { effectApp } = makeEffectIt({
|
|
49
|
+
layer: Layer.succeed(Token, "from-layer"),
|
|
50
|
+
makeHarness: (context) => Effect.gen(function* () {
|
|
51
|
+
return {
|
|
52
|
+
name: context.task.name,
|
|
53
|
+
token: yield* Token,
|
|
54
|
+
};
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
effectApp("reads the fixture's services", function* (harness, context) {
|
|
59
|
+
expect(harness.token).toBe("from-layer");
|
|
60
|
+
expect(harness.name).toBe(context.task.name);
|
|
61
|
+
expect(yield* Token).toBe("from-layer");
|
|
62
|
+
});
|
|
35
63
|
```
|
|
36
64
|
|
|
37
|
-
The
|
|
38
|
-
installs TestClock and the other test services, `live` keeps the live ones, and
|
|
39
|
-
each runs the body in a Scope. A function that returns an Effect works as well,
|
|
40
|
-
as with `@effect/vitest`. Each tester supports `each`, `fails`, `only`, `runIf`,
|
|
41
|
-
`skip` and `skipIf`; an `each` body receives the table case.
|
|
65
|
+
The Layer supplies `Token`. `makeHarness` runs for the test and reads that same service; the generator receives its result and can also yield `Token` directly. Harness fields and provided services keep their types. A body that requires a service the Layer does not provide does not type-check.
|
|
42
66
|
|
|
43
|
-
|
|
67
|
+
A fixture that needs no services uses `Layer.empty`. This separate module
|
|
68
|
+
replaces the token fixture above:
|
|
44
69
|
|
|
45
70
|
```ts
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
71
|
+
import { expect } from "@effect/vitest";
|
|
72
|
+
import { Effect, Layer } from "effect";
|
|
73
|
+
import { makeEffectIt } from "@shivaedev/effect-test/vitest.ts";
|
|
48
74
|
|
|
49
75
|
const { effectApp } = makeEffectIt({
|
|
50
|
-
layer:
|
|
51
|
-
makeHarness: () => Effect.succeed({}),
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
effectApp("
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})
|
|
76
|
+
layer: Layer.empty,
|
|
77
|
+
makeHarness: () => Effect.succeed({ shelf: 0 }),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
effectApp("uses a harness without services", function* (harness) {
|
|
81
|
+
expect(harness.shelf).toBe(0);
|
|
82
|
+
});
|
|
58
83
|
```
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
There is no placeholder service. The harness is still inferred, and a body that tries to yield `Token` from this fixture does not compile. The Layer itself must have no unmet service requirements: compose its dependencies before passing it to the factory.
|
|
86
|
+
|
|
87
|
+
#### Wrapping the harness and body
|
|
88
|
+
|
|
89
|
+
This alternative token fixture adds a tracing wrapper:
|
|
62
90
|
|
|
63
91
|
```ts
|
|
92
|
+
import { expect } from "@effect/vitest";
|
|
93
|
+
import { Context, Effect, Layer } from "effect";
|
|
94
|
+
import { makeEffectIt } from "@shivaedev/effect-test/vitest.ts";
|
|
95
|
+
|
|
96
|
+
class Token extends Context.Service<Token, string>()("test/Token") {}
|
|
97
|
+
|
|
64
98
|
const { effectApp } = makeEffectIt({
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
makeHarness: () =>
|
|
68
|
-
})
|
|
99
|
+
around: (effect) => Effect.withSpan(effect, "test.token"),
|
|
100
|
+
layer: Layer.succeed(Token, "from-layer"),
|
|
101
|
+
makeHarness: () => Effect.service(Token),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
effectApp("reads the harness", function* (token) {
|
|
105
|
+
expect(token).toBe("from-layer");
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`around` receives the Effect that makes the harness and then runs the body. The wrapper also has the Layer's services. This is where a fixture composes its tracing or transaction boundary. The database package owns whether that transaction rolls back; this runner does not add a database policy. The later examples use the first token fixture, whose harness contains `token` and `name`.
|
|
110
|
+
|
|
111
|
+
`@shivaedev/effect-prisma` and `@shivaedev/effect-trpc` build their testing helpers on this factory. `@shivaedev/test-story` builds domain stories above it and owns traits, verbs, engine stepping and failure narration. Use those helpers when the test needs their vocabulary; use this runner when defining the fixture underneath. An application-owned story kit can lease PostgreSQL through its worker Layer, use `around` for real-commit resets or local rollback, and acquire servers, native RPC clients and React roots through per-test Scope. The [order feature kit](https://github.com/ShivaeDev/platform/tree/main/packages/effect-react/src/test-support/order-example) demonstrates that composition; [test-story’s kit guide](https://github.com/ShivaeDev/platform/tree/main/packages/test-story#whole-stack-application-kits) explains the application’s isolation, seed and screen responsibilities.
|
|
112
|
+
|
|
113
|
+
### 2. In every test: the generator
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { expect } from "@effect/vitest";
|
|
117
|
+
import { Effect } from "effect";
|
|
118
|
+
import { it } from "@shivaedev/effect-test/it.ts";
|
|
119
|
+
|
|
120
|
+
it.effect("reads an Effect result", function* ({ task }) {
|
|
121
|
+
expect(task.name).toBe("reads an Effect result");
|
|
122
|
+
expect(yield* Effect.succeed(3)).toBe(3);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it.effect("accepts an Effect-returning body", () =>
|
|
126
|
+
Effect.sync(() => expect(3).toBe(3)),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
it.effect.each([1, 2])("passes the case %s", function* (count) {
|
|
130
|
+
expect(yield* Effect.succeed(count)).toBe(count);
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The ordinary body receives Vitest's context. An `each` body receives the table case instead. Both generator and Effect-returning bodies are accepted. `it` itself remains the plain Vitest tester.
|
|
135
|
+
|
|
136
|
+
For a fixture's `effectApp.each`, the arguments are the case, harness and context:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
effectApp.each(["alpha", "beta"])("checks %s", function* (item, harness, context) {
|
|
140
|
+
expect(context.task.name).toContain(item);
|
|
141
|
+
expect(harness.token).toBe("from-layer");
|
|
142
|
+
});
|
|
69
143
|
```
|
|
70
144
|
|
|
71
|
-
`
|
|
145
|
+
This uses the token fixture from the first example. Case and harness types are inferred. The fixture tester, `it.effect` and `it.live` accept generator bodies and Effect-returning functions. A synchronous DOM fixture test can return `Effect.sync(() => expect(harness.token).toBe("from-layer"))`; the harness, services and per-test Scope are the same for both body forms.
|
|
72
146
|
|
|
73
|
-
|
|
147
|
+
### Choosing which clock runs the test
|
|
74
148
|
|
|
75
|
-
|
|
76
|
-
on a single test when the program must use wall-clock time:
|
|
149
|
+
`it.effect` uses TestClock; `it.live` uses live time. A fixture defaults to `clock: "test"`. Its factory may choose `clock: "live"`, and a single test may override the factory in either direction:
|
|
77
150
|
|
|
78
151
|
```ts
|
|
79
|
-
effectApp("
|
|
80
|
-
yield*
|
|
81
|
-
}, { clock: "live" })
|
|
152
|
+
effectApp("reads live time", function* () {
|
|
153
|
+
expect(yield* Clock.currentTimeMillis).toBeGreaterThan(1_000_000);
|
|
154
|
+
}, { clock: "live" });
|
|
82
155
|
```
|
|
83
156
|
|
|
84
|
-
The
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
157
|
+
The override changes the per-test environment used by the wrapper, harness and body. TestConsole is installed for both clock choices.
|
|
158
|
+
|
|
159
|
+
Worker acquisition is separate: a Layer that reads the default Clock while it is being built sees live time, even when its tests use TestClock. A fiber started during that acquisition retains its worker environment, so advancing a test's clock does not advance that fiber's timers. A Layer that supplies its own Clock also supplies that service through its Context; the test's clock option chooses the outer environment and does not replace services explicitly supplied by the fixture.
|
|
160
|
+
|
|
161
|
+
### Waiting for a typed readiness condition
|
|
89
162
|
|
|
90
|
-
|
|
163
|
+
`eventually(effect, options)` retries the Effect's typed failures until it succeeds or uses its retry budget. Under TestClock it advances time with `TestClock.adjust`; under live time it retries with `Schedule.spaced`.
|
|
91
164
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
165
|
+
```ts
|
|
166
|
+
import { eventually } from "@shivaedev/effect-test/eventually.ts";
|
|
167
|
+
|
|
168
|
+
effectApp("reaches the selected time", function* () {
|
|
169
|
+
const readyAt = yield* eventually(
|
|
170
|
+
Effect.gen(function* () {
|
|
171
|
+
const now = yield* Clock.currentTimeMillis;
|
|
172
|
+
return now >= 50 ? now : yield* Effect.fail("too-early");
|
|
173
|
+
}),
|
|
174
|
+
{ interval: "7 millis", times: 8 },
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(readyAt).toBe(56);
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The attempts see 0, 7, 14 and subsequent multiples of 7 through 56 milliseconds. `times` counts retries after the initial attempt, so `times: 0` tries once. When the budget runs out, the final typed failure remains the failure.
|
|
182
|
+
|
|
183
|
+
A thrown assertion is a defect, so it escapes on the first attempt. Interruption also escapes immediately. Capture an assertion with `Effect.try` only when its failure means the state is not ready yet:
|
|
98
184
|
|
|
99
185
|
```ts
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
186
|
+
effectApp("retries a readiness assertion", function* () {
|
|
187
|
+
let attempts = 0;
|
|
188
|
+
|
|
189
|
+
yield* eventually(
|
|
190
|
+
Effect.try(() => {
|
|
191
|
+
attempts += 1;
|
|
192
|
+
expect(attempts).toBe(3);
|
|
193
|
+
}),
|
|
194
|
+
{ interval: "1 millis", times: 3 },
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(attempts).toBe(3);
|
|
198
|
+
});
|
|
109
199
|
```
|
|
200
|
+
|
|
201
|
+
This distinction applies under both clocks. The helper preserves the Effect's success, error and service types.
|
|
202
|
+
|
|
203
|
+
### API
|
|
204
|
+
|
|
205
|
+
Import the module that defines the name; the package has no root entry.
|
|
206
|
+
|
|
207
|
+
| Module | Exports |
|
|
208
|
+
| --- | --- |
|
|
209
|
+
| `@shivaedev/effect-test/it.ts` | `it`, with generator-aware `effect` and `live` testers. |
|
|
210
|
+
| `@shivaedev/effect-test/vitest.ts` | `makeEffectIt({ layer, makeHarness, around?, clock? })`, returning `{ effectApp }`. |
|
|
211
|
+
| `@shivaedev/effect-test/eventually.ts` | `eventually(effect, options?)` and `EventuallyOptions`. |
|
|
212
|
+
| `@shivaedev/effect-test/any-test-layer.ts` | `AnyTestLayer` and `buildTestLayer(layer, scope)`, an Effect that builds the Layer's service Context in the supplied scope. |
|
|
213
|
+
| `@shivaedev/effect-test/types.ts` | `EffectClock`, `EffectTestOptions`, `EffectTestBody`, `EffectTest`, `EffectTester`, `MakeEffectItOptions`, `MakeEffectItResult`, `EffectItTest`, `EffectItTester` and `EffectIt`. |
|
|
214
|
+
|
|
215
|
+
`effectApp(name, body, options?)` takes a generator or Effect-returning body with `(harness, context)`. Its `each(cases)` takes `(item, harness, context)`. Options are a timeout in milliseconds or Vitest test options with an optional `clock`.
|
|
216
|
+
|
|
217
|
+
`it.effect` and `it.live` take `(name, body, options?)`; options are a timeout or Vitest test options. A regular body takes the context; an `each` body takes the case. These testers and `effectApp` expose `each`, `fails`, `only`, `runIf`, `skip` and `skipIf`.
|
|
218
|
+
|
|
219
|
+
`EventuallyOptions` has `interval?: Duration.Input` and `times?: number`. The default interval is `"10 millis"`; omitting `times` leaves retries unbounded.
|
|
220
|
+
|
|
221
|
+
### Install, setup and limits
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
pnpm add --save-dev @shivaedev/effect-test @effect/vitest@4.0.0-rc.112 effect@4.0.0-rc.112 vitest@4.1.11
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The package declares `effect` as a peer and `@effect/vitest` as an optional peer. The Vitest runner modules require `@effect/vitest`; install it when using them. The package requires Node.js 24 or later. Use peer versions compatible with the package's `package.json`.
|
|
228
|
+
|
|
229
|
+
- Define a fixture tester in test support and import it from Vitest tests. Calling `makeEffectIt` declares a Vitest fixture; it is not an application runtime factory.
|
|
230
|
+
- The Layer is shared within its worker fixture. Reset application state in the harness or compose the fixture's own isolation wrapper when tests need it.
|
|
231
|
+
- TestClock controls its Effect environment. It does not establish how external timers, real HTTP or a database behave.
|
|
232
|
+
- An unbounded readiness poll can run until Vitest ends the test. Set a retry budget when the condition may never become true.
|
|
233
|
+
- Compiler tests prove service requirements and inferred types; runtime tests prove the exercised clock and failure paths. Neither is evidence that a consumer's composed application has been validated.
|
package/dist/eventually.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eventually.d.ts","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,EAAoB,MAAM,QAAQ,CAAC;AAGxE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"eventually.d.ts","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,EAAoB,MAAM,QAAQ,CAAC;AAGxE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAoCD,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,UAAU,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,YAAY,iBAAiB,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAMpH,CAAC"}
|
package/dist/eventually.js
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
import { Clock, Effect, Result, Schedule } from "effect";
|
|
2
2
|
import * as TestClock from "effect/testing/TestClock";
|
|
3
3
|
const defaultInterval = "10 millis";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
4
|
+
function isTestClock(clock) {
|
|
5
|
+
return "adjust" in clock;
|
|
6
|
+
}
|
|
7
|
+
function retryOnLiveClock(effect, interval, times) {
|
|
8
|
+
return times === undefined
|
|
9
|
+
? Effect.retry(effect, Schedule.spaced(interval))
|
|
10
|
+
: Effect.retry(effect, {
|
|
11
|
+
schedule: Schedule.spaced(interval),
|
|
12
|
+
times,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function retryOnTestClock(effect, interval, times) {
|
|
16
|
+
return Effect.gen(function* () {
|
|
17
|
+
let retriesLeft = times;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const result = yield* Effect.result(effect);
|
|
20
|
+
if (Result.isSuccess(result)) {
|
|
21
|
+
return result.success;
|
|
22
|
+
}
|
|
23
|
+
if (retriesLeft === 0) {
|
|
24
|
+
return yield* Effect.fail(result.failure);
|
|
25
|
+
}
|
|
26
|
+
if (retriesLeft !== undefined) {
|
|
27
|
+
retriesLeft -= 1;
|
|
28
|
+
}
|
|
29
|
+
yield* TestClock.adjust(interval);
|
|
23
30
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
27
33
|
export const eventually = (effect, options) => Effect.gen(function* () {
|
|
28
34
|
const interval = options?.interval ?? defaultInterval;
|
|
29
35
|
const clock = yield* Clock.Clock;
|
package/dist/eventually.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eventually.js","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAiB,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACxE,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AAOtD,MAAM,eAAe,GAAG,WAAW,CAAC;AAEpC,
|
|
1
|
+
{"version":3,"file":"eventually.js","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAiB,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACxE,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AAOtD,MAAM,eAAe,GAAG,WAAW,CAAC;AAEpC,SAAS,WAAW,CAAC,KAAkB;IACtC,OAAO,QAAQ,IAAI,KAAK,CAAC;AAC1B,CAAC;AAED,SAAS,gBAAgB,CAAU,MAA8B,EAAE,QAAwB,EAAE,KAAyB;IACrH,OAAO,KAAK,KAAK,SAAS;QACzB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACjD,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE;YACrB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;YACnC,KAAK;SACL,CAAC,CAAC;AACN,CAAC;AAED,SAAS,gBAAgB,CAAU,MAA8B,EAAE,QAAwB,EAAE,KAAyB;IACrH,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC1B,IAAI,WAAW,GAAG,KAAK,CAAC;QACxB,SAAS,CAAC;YACT,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC5C,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC9B,OAAO,MAAM,CAAC,OAAO,CAAC;YACvB,CAAC;YACD,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;gBACvB,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC3C,CAAC;YACD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;gBAC/B,WAAW,IAAI,CAAC,CAAC;YAClB,CAAC;YACD,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,CAAC;IACF,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,CAAU,MAA8B,EAAE,OAA2B,EAA0B,EAAE,CAC1H,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACnB,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,eAAe,CAAC;IACtD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,CAAC;IACvE,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;AACvD,CAAC,CAAC,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -6,23 +6,24 @@ export type EffectClock = "test" | "live";
|
|
|
6
6
|
export type EffectTestOptions = TestOptions & {
|
|
7
7
|
readonly clock?: EffectClock;
|
|
8
8
|
};
|
|
9
|
-
export type
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
readonly
|
|
16
|
-
readonly
|
|
9
|
+
export type EffectTestBody<THarness, TProvided, A = unknown> = (harness: THarness, context: TestContext) => Generator<Effect.Effect<unknown, unknown, TProvided | Scope.Scope>, A, never> | Effect.Effect<A, unknown, TProvided | Scope.Scope>;
|
|
10
|
+
export type EffectTest<THarness, TProvided> = <A, TEff extends Effect.Effect<unknown, unknown, TProvided | Scope.Scope>>(name: string, body: (harness: THarness, context: TestContext) => Generator<TEff, A, never> | Effect.Effect<A, unknown, TProvided | Scope.Scope>, options?: number | EffectTestOptions) => void;
|
|
11
|
+
export interface EffectTester<THarness, TProvided> extends EffectTest<THarness, TProvided> {
|
|
12
|
+
readonly each: <TItem>(cases: readonly TItem[]) => <A, TEff extends Effect.Effect<unknown, unknown, TProvided | Scope.Scope>>(name: string, body: (item: TItem, harness: THarness, context: TestContext) => Generator<TEff, A, never> | Effect.Effect<A, unknown, TProvided | Scope.Scope>, options?: number | EffectTestOptions) => void;
|
|
13
|
+
readonly fails: EffectTest<THarness, TProvided>;
|
|
14
|
+
readonly only: EffectTest<THarness, TProvided>;
|
|
15
|
+
readonly runIf: (condition: unknown) => EffectTest<THarness, TProvided>;
|
|
16
|
+
readonly skip: EffectTest<THarness, TProvided>;
|
|
17
|
+
readonly skipIf: (condition: unknown) => EffectTest<THarness, TProvided>;
|
|
17
18
|
}
|
|
18
|
-
export interface MakeEffectItOptions<
|
|
19
|
-
readonly around?: (<A, E>(effect: Effect.Effect<A, E, Layer.Success<
|
|
19
|
+
export interface MakeEffectItOptions<THarness, TTestLayer extends AnyTestLayer> {
|
|
20
|
+
readonly around?: (<A, E>(effect: Effect.Effect<A, E, Layer.Success<TTestLayer> | Scope.Scope>) => Effect.Effect<A, unknown, Layer.Success<TTestLayer> | Scope.Scope>) | undefined;
|
|
20
21
|
readonly clock?: EffectClock | undefined;
|
|
21
|
-
readonly layer:
|
|
22
|
-
readonly makeHarness: (context: TestContext) => Effect.Effect<
|
|
22
|
+
readonly layer: TTestLayer;
|
|
23
|
+
readonly makeHarness: (context: TestContext) => Effect.Effect<THarness, unknown, Layer.Success<TTestLayer> | Scope.Scope>;
|
|
23
24
|
}
|
|
24
|
-
export interface MakeEffectItResult<
|
|
25
|
-
readonly effectApp: EffectTester<
|
|
25
|
+
export interface MakeEffectItResult<THarness, TProvided> {
|
|
26
|
+
readonly effectApp: EffectTester<THarness, TProvided>;
|
|
26
27
|
}
|
|
27
28
|
export interface EffectItTest<R, TArgs extends readonly unknown[] = [context: TestContext]> {
|
|
28
29
|
<TEffect extends Effect.Effect<unknown, unknown, R>, A>(name: string, body: (...args: TArgs) => Generator<TEffect, A, never>, options?: number | TestOptions): void;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1C,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/vitest.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
2
|
import { type AnyTestLayer } from "./any-test-layer.ts";
|
|
3
3
|
import type { MakeEffectItOptions, MakeEffectItResult } from "./types.ts";
|
|
4
|
-
export declare const makeEffectIt: <
|
|
4
|
+
export declare const makeEffectIt: <THarness, TTestLayer extends AnyTestLayer>(options: MakeEffectItOptions<THarness, TTestLayer>) => MakeEffectItResult<THarness, Layer.Success<TTestLayer>>;
|
|
5
5
|
//# sourceMappingURL=vitest.d.ts.map
|
package/dist/vitest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AACA,OAAO,EAAqC,KAAK,EAAS,MAAM,QAAQ,CAAC;AAGzE,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AACA,OAAO,EAAqC,KAAK,EAAS,MAAM,QAAQ,CAAC;AAGzE,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAA4E,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAqEpJ,eAAO,MAAM,YAAY,GAAI,QAAQ,EAAE,UAAU,SAAS,YAAY,WAC5D,mBAAmB,CAAC,QAAQ,EAAE,UAAU,CAAC,KAChD,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAuExD,CAAC"}
|
package/dist/vitest.js
CHANGED
|
@@ -6,11 +6,15 @@ import { buildTestLayer } from "./any-test-layer.js";
|
|
|
6
6
|
const fixtureName = "__effectTestContext";
|
|
7
7
|
const TestClockEnvironment = Layer.mergeAll(TestConsole.layer, TestClock.layer());
|
|
8
8
|
const LiveClockEnvironment = TestConsole.layer;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
function environmentFor(clock) {
|
|
10
|
+
return clock === "live" ? LiveClockEnvironment : TestClockEnvironment;
|
|
11
|
+
}
|
|
12
|
+
function restoreContext(fixture, context) {
|
|
13
|
+
return {
|
|
14
|
+
...context,
|
|
15
|
+
[fixtureName]: fixture,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
14
18
|
const runEffectTest = (effect, context, clock) => Effect.runPromise(Effect.gen(function* () {
|
|
15
19
|
const exit = yield* Effect.exit(effect);
|
|
16
20
|
if (Exit.isFailure(exit)) {
|
|
@@ -20,7 +24,7 @@ const runEffectTest = (effect, context, clock) => Effect.runPromise(Effect.gen(f
|
|
|
20
24
|
}
|
|
21
25
|
return yield* exit;
|
|
22
26
|
}).pipe(Effect.scoped, Effect.provide(environmentFor(clock))), { signal: context.signal });
|
|
23
|
-
|
|
27
|
+
function splitOptions(options) {
|
|
24
28
|
if (typeof options === "number") {
|
|
25
29
|
return { clock: undefined, vitest: { timeout: options } };
|
|
26
30
|
}
|
|
@@ -29,46 +33,51 @@ const splitOptions = (options) => {
|
|
|
29
33
|
}
|
|
30
34
|
const { clock, ...vitest } = options;
|
|
31
35
|
return { clock, vitest };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
36
|
+
}
|
|
37
|
+
function makeFixtureIt(layer) {
|
|
38
|
+
return effectIt.extend(fixtureName, { scope: "worker" }, async ({}, { onCleanup }) => {
|
|
39
|
+
const scope = Effect.runSync(Scope.make());
|
|
40
|
+
onCleanup(() => Effect.runPromise(Scope.close(scope, Exit.void)));
|
|
41
|
+
try {
|
|
42
|
+
return await Effect.runPromise(buildTestLayer(layer, scope));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
await Effect.runPromise(Scope.close(scope, Exit.void));
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
44
50
|
export const makeEffectIt = (options) => {
|
|
45
51
|
const fixtureIt = makeFixtureIt(options.layer);
|
|
46
52
|
const defaultClock = options.clock ?? "test";
|
|
47
|
-
|
|
53
|
+
function run(body, context) {
|
|
48
54
|
const ready = Effect.gen(function* () {
|
|
49
55
|
const harness = yield* options.makeHarness(context);
|
|
50
|
-
|
|
56
|
+
const result = body(harness, context);
|
|
57
|
+
return yield* Effect.isEffect(result) ? result : Effect.gen(() => result);
|
|
51
58
|
});
|
|
52
59
|
const wrapped = options.around?.(ready) ?? ready;
|
|
53
60
|
return wrapped.pipe(Effect.provide(context[fixtureName]));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
}
|
|
62
|
+
function register(current) {
|
|
63
|
+
return (name, body, testOptions) => {
|
|
64
|
+
const split = splitOptions(testOptions);
|
|
65
|
+
const clock = split.clock ?? defaultClock;
|
|
66
|
+
current(name, split.vitest, ({ __effectTestContext, task, signal, onTestFailed, onTestFinished, skip, annotate, expect, _local }) => {
|
|
67
|
+
const context = restoreContext(__effectTestContext, {
|
|
68
|
+
_local,
|
|
69
|
+
annotate,
|
|
70
|
+
expect,
|
|
71
|
+
onTestFailed,
|
|
72
|
+
onTestFinished,
|
|
73
|
+
signal,
|
|
74
|
+
skip,
|
|
75
|
+
task,
|
|
76
|
+
});
|
|
77
|
+
return runEffectTest(run(body, context), context, clock);
|
|
68
78
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
79
|
+
};
|
|
80
|
+
}
|
|
72
81
|
const effectApp = Object.assign(register(fixtureIt), {
|
|
73
82
|
each: (cases) => (name, body, testOptions) => {
|
|
74
83
|
const split = splitOptions(testOptions);
|
package/dist/vitest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest.js","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,IAAI,QAAQ,EAAsC,MAAM,gBAAgB,CAAC;AACpF,OAAO,EAAE,KAAK,EAAgB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAqB,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGxE,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAQ1C,MAAM,oBAAoB,GAAG,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC;AAClF,MAAM,oBAAoB,GAAG,WAAW,CAAC,KAAK,CAAC;AAE/C,
|
|
1
|
+
{"version":3,"file":"vitest.js","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# North star
|
|
2
|
+
|
|
3
|
+
## The problem
|
|
4
|
+
|
|
5
|
+
An Effect test needs more than a Promise runner. It needs the application's services, a scope for work that must be released, a clock it can control and a way to wait for a typed readiness condition. When each database or transport fixture writes that machinery, small differences become differences in correctness: a clock advances in one helper but waits in another, a retry catches a defect, or a service is acquired at the wrong lifetime.
|
|
6
|
+
|
|
7
|
+
The package gives those fixtures one runner. A test names its work as a generator, and a fixture supplies its Layer and the value the test receives. The test still uses Effect's services, failure channel and clock, and Vitest still declares and reports the test.
|
|
8
|
+
|
|
9
|
+
## The ideal
|
|
10
|
+
|
|
11
|
+
A test contains the work specific to that test. Shared service acquisition, per-test harness construction and an optional wrapper are defined once in test support. A plain Effect test needs no custom fixture: its generator goes directly to `it.effect` or `it.live`.
|
|
12
|
+
|
|
13
|
+
The boundary between worker and test is explicit. A worker owns the acquired service Context, while a test owns its harness and execution scope. A shared database service does not imply fresh database contents, and a fresh harness does not imply a freshly acquired service. The fixture that owns application state chooses how to reset or transact it.
|
|
14
|
+
|
|
15
|
+
Time is part of the Effect environment. A controlled test clock makes a readiness poll cheap and inspectable; live time remains an explicit choice for work that needs it. A typed readiness failure means “try again”. A defect means something broke, and interruption means the run must stop. These are different outcomes and the runner must keep them different.
|
|
16
|
+
|
|
17
|
+
## What good looks like
|
|
18
|
+
|
|
19
|
+
- A generator reaches the application's real services instead of converting them into a separate testing API.
|
|
20
|
+
- A Layer, a harness and a wrapper are enough to define a reusable fixture. Their types are inferred, and a body cannot silently ask for a service the fixture does not provide.
|
|
21
|
+
- Resource lifetimes are visible in the design and backed by tests at the boundaries where this package adds behavior.
|
|
22
|
+
- Clock selection does not imply control over resources that were acquired in a different environment.
|
|
23
|
+
- A failed assertion is retried only when the author explicitly makes it a typed, retryable failure.
|
|
24
|
+
- Downstream fixture authors reuse the runner and own their domain setup.
|
|
25
|
+
|
|
26
|
+
## Trade-offs
|
|
27
|
+
|
|
28
|
+
When goals conflict, preserve native Effect semantics first, keep one runner second, preserve deliberate worker and test lifetimes third, prove readable behavior fourth, and keep the adapter small fifth.
|
|
29
|
+
|
|
30
|
+
**Shared acquisition costs less and requires state discipline.** A worker Layer avoids rebuilding expensive services for every test. That places application-state isolation in the fixture that understands the state. Adding a hidden reset would conceal the lifetime rather than solve it.
|
|
31
|
+
|
|
32
|
+
**Controlled time and live time solve different problems.** A test clock helps a test advance the Effects in its environment. It does not make external timers or a worker's already-running fibers deterministic. Prefer a precise limit over a broad promise of determinism.
|
|
33
|
+
|
|
34
|
+
**Composition keeps responsibility local.** An `around` wrapper can establish a transaction or tracing boundary for the harness and body. This package owns how the wrapper fits the runner; the package that supplies a transaction owns its rollback semantics.
|
|
35
|
+
|
|
36
|
+
**Types are a contract, not runtime evidence.** Compiler tests establish which requirements, harness fields and table cases a body may use. Runtime tests establish what runs, when it runs and which failure escapes. Neither substitutes for the other.
|
|
37
|
+
|
|
38
|
+
## What it leaves out
|
|
39
|
+
|
|
40
|
+
- Database rollback, migrations, seeded application state and transport caller construction. Those belong to their integrations or application test support.
|
|
41
|
+
- Domain traits, verbs, engine stepping and story failure narration. `@shivaedev/test-story` owns that layer above this runner.
|
|
42
|
+
- A replacement for Vitest's reporting, selection or configuration, or for Effect's Context, Scope and scheduling.
|
|
43
|
+
- Evidence of real HTTP, PostgreSQL, browser or deployment behavior from a controlled-clock unit test. Composed examples and host-boundary validation belong to the framework.
|
package/docs/roadmap.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
This roadmap owns the test runner, its public contracts and its package tests. The [framework roadmap](https://github.com/ShivaeDev/platform/blob/main/docs/framework/roadmap.md) owns composed fixtures, application adoption and host or deployment validation. The same status is recorded in one place.
|
|
4
|
+
|
|
5
|
+
## Built
|
|
6
|
+
|
|
7
|
+
- [x] `it.effect` and `it.live` accept generator bodies and Effect-returning bodies over the upstream testers. Tests cover the controlled and live clocks, table cases, expected failure, conditional skipping and plain Vitest tests.
|
|
8
|
+
- [x] `makeEffectIt` supplies the Layer's services, an inferred harness and Vitest's test context to the generator. Tests cover wrapper entry, harness values and table cases.
|
|
9
|
+
- [x] Default TestClock, factory live-clock selection and per-test overrides in either direction. Acquisition-time tests distinguish the worker Layer's live clock from the harness and body's test clock.
|
|
10
|
+
- [x] `Layer.empty` runs without a placeholder service. Compiler tests reject a service the empty Layer does not provide and retain harness, service and table-case types.
|
|
11
|
+
- [x] `eventually` retries typed failures on both clock paths. Tests count initial attempts and retries, preserve the final typed failure, reject thrown assertion retries, accept assertions captured with `Effect.try`, and preserve interruption.
|
|
12
|
+
- [x] Packed-consumer compiler fixtures exercise the published runner modules and retained harness types.
|
|
13
|
+
|
|
14
|
+
## Next
|
|
15
|
+
|
|
16
|
+
- [ ] Add direct acquisition-count and finalizer assertions for worker Layer reuse and release, including acquisition failure.
|
|
17
|
+
- [ ] Assert per-test scope cleanup and wrapper ordering around harness construction as well as the body.
|
|
18
|
+
- [ ] Cover a timer fiber started during worker acquisition and a fixture that explicitly supplies its own Clock, so the clock boundary has direct regression evidence.
|
|
19
|
+
- [ ] Cover the runner's TestConsole environment, tester variants and `buildTestLayer` with assertions dedicated to the behavior this package adds.
|
|
20
|
+
|
|
21
|
+
## Open questions
|
|
22
|
+
|
|
23
|
+
- Should composed application fixtures gain any API here, or remain framework examples and downstream helpers? The runner keeps the composition points small; new fixture APIs need a repeated job that the existing Layer, harness and wrapper cannot express.
|
|
24
|
+
- Which lifecycle and clock properties need local regression tests, and which should rely on the upstream Vitest and Effect contracts? The next items identify the gaps in this package's own evidence without changing its public API.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-test",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Generic Effect Vitest runner with worker-scoped Layers and TestClock",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-test#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -37,13 +37,14 @@
|
|
|
37
37
|
"!src/**/*.test.ts",
|
|
38
38
|
"!src/**/*.spec.ts",
|
|
39
39
|
"!src/**/test-support",
|
|
40
|
+
"docs",
|
|
40
41
|
"CHANGELOG.md",
|
|
41
42
|
"README.md"
|
|
42
43
|
],
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@effect/vitest": "4.0.0-rc.112",
|
|
45
|
-
"@shivaedev/quality": "0.
|
|
46
|
-
"@types/node": "24.
|
|
46
|
+
"@shivaedev/quality": "0.19.0",
|
|
47
|
+
"@types/node": "24.19.1",
|
|
47
48
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
48
49
|
"effect": "4.0.0-rc.112",
|
|
49
50
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
"provenance": true
|
|
67
68
|
},
|
|
68
69
|
"scripts": {
|
|
69
|
-
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" &&
|
|
70
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
|
|
70
71
|
"ready": "pnpm typecheck && pnpm test && pnpm build",
|
|
71
72
|
"test": "vitest run",
|
|
72
73
|
"typecheck": "tsc --noEmit"
|
package/src/eventually.ts
CHANGED
|
@@ -8,18 +8,21 @@ export interface EventuallyOptions {
|
|
|
8
8
|
|
|
9
9
|
const defaultInterval = "10 millis";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
function isTestClock(clock: Clock.Clock): clock is TestClock.TestClock {
|
|
12
|
+
return "adjust" in clock;
|
|
13
|
+
}
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
times === undefined
|
|
15
|
+
function retryOnLiveClock<A, E, R>(effect: Effect.Effect<A, E, R>, interval: Duration.Input, times: number | undefined): Effect.Effect<A, E, R> {
|
|
16
|
+
return times === undefined
|
|
15
17
|
? Effect.retry(effect, Schedule.spaced(interval))
|
|
16
18
|
: Effect.retry(effect, {
|
|
17
19
|
schedule: Schedule.spaced(interval),
|
|
18
20
|
times,
|
|
19
21
|
});
|
|
22
|
+
}
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
Effect.gen(function* () {
|
|
24
|
+
function retryOnTestClock<A, E, R>(effect: Effect.Effect<A, E, R>, interval: Duration.Input, times: number | undefined): Effect.Effect<A, E, R> {
|
|
25
|
+
return Effect.gen(function* () {
|
|
23
26
|
let retriesLeft = times;
|
|
24
27
|
for (;;) {
|
|
25
28
|
const result = yield* Effect.result(effect);
|
|
@@ -35,6 +38,7 @@ const retryOnTestClock = <A, E, R>(effect: Effect.Effect<A, E, R>, interval: Dur
|
|
|
35
38
|
yield* TestClock.adjust(interval);
|
|
36
39
|
}
|
|
37
40
|
});
|
|
41
|
+
}
|
|
38
42
|
|
|
39
43
|
export const eventually = <A, E, R>(effect: Effect.Effect<A, E, R>, options?: EventuallyOptions): Effect.Effect<A, E, R> =>
|
|
40
44
|
Effect.gen(function* () {
|
package/src/types.ts
CHANGED
|
@@ -9,38 +9,45 @@ export type EffectTestOptions = TestOptions & {
|
|
|
9
9
|
readonly clock?: EffectClock;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export type
|
|
12
|
+
export type EffectTestBody<THarness, TProvided, A = unknown> = (
|
|
13
|
+
harness: THarness,
|
|
14
|
+
context: TestContext,
|
|
15
|
+
) => Generator<Effect.Effect<unknown, unknown, TProvided | Scope.Scope>, A, never> | Effect.Effect<A, unknown, TProvided | Scope.Scope>;
|
|
16
|
+
|
|
17
|
+
export type EffectTest<THarness, TProvided> = <A, TEff extends Effect.Effect<unknown, unknown, TProvided | Scope.Scope>>(
|
|
13
18
|
name: string,
|
|
14
|
-
body: (harness:
|
|
19
|
+
body: (harness: THarness, context: TestContext) => Generator<TEff, A, never> | Effect.Effect<A, unknown, TProvided | Scope.Scope>,
|
|
15
20
|
options?: number | EffectTestOptions,
|
|
16
21
|
) => void;
|
|
17
22
|
|
|
18
|
-
export interface EffectTester<
|
|
19
|
-
readonly each: <
|
|
20
|
-
cases: readonly
|
|
21
|
-
) => <A,
|
|
23
|
+
export interface EffectTester<THarness, TProvided> extends EffectTest<THarness, TProvided> {
|
|
24
|
+
readonly each: <TItem>(
|
|
25
|
+
cases: readonly TItem[],
|
|
26
|
+
) => <A, TEff extends Effect.Effect<unknown, unknown, TProvided | Scope.Scope>>(
|
|
22
27
|
name: string,
|
|
23
|
-
body: (item:
|
|
28
|
+
body: (item: TItem, harness: THarness, context: TestContext) => Generator<TEff, A, never> | Effect.Effect<A, unknown, TProvided | Scope.Scope>,
|
|
24
29
|
options?: number | EffectTestOptions,
|
|
25
30
|
) => void;
|
|
26
|
-
readonly fails: EffectTest<
|
|
27
|
-
readonly only: EffectTest<
|
|
28
|
-
readonly runIf: (condition: unknown) => EffectTest<
|
|
29
|
-
readonly skip: EffectTest<
|
|
30
|
-
readonly skipIf: (condition: unknown) => EffectTest<
|
|
31
|
+
readonly fails: EffectTest<THarness, TProvided>;
|
|
32
|
+
readonly only: EffectTest<THarness, TProvided>;
|
|
33
|
+
readonly runIf: (condition: unknown) => EffectTest<THarness, TProvided>;
|
|
34
|
+
readonly skip: EffectTest<THarness, TProvided>;
|
|
35
|
+
readonly skipIf: (condition: unknown) => EffectTest<THarness, TProvided>;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
export interface MakeEffectItOptions<
|
|
38
|
+
export interface MakeEffectItOptions<THarness, TTestLayer extends AnyTestLayer> {
|
|
34
39
|
readonly around?:
|
|
35
|
-
| (<A, E>(
|
|
40
|
+
| (<A, E>(
|
|
41
|
+
effect: Effect.Effect<A, E, Layer.Success<TTestLayer> | Scope.Scope>,
|
|
42
|
+
) => Effect.Effect<A, unknown, Layer.Success<TTestLayer> | Scope.Scope>)
|
|
36
43
|
| undefined;
|
|
37
44
|
readonly clock?: EffectClock | undefined;
|
|
38
|
-
readonly layer:
|
|
39
|
-
readonly makeHarness: (context: TestContext) => Effect.Effect<
|
|
45
|
+
readonly layer: TTestLayer;
|
|
46
|
+
readonly makeHarness: (context: TestContext) => Effect.Effect<THarness, unknown, Layer.Success<TTestLayer> | Scope.Scope>;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
export interface MakeEffectItResult<
|
|
43
|
-
readonly effectApp: EffectTester<
|
|
49
|
+
export interface MakeEffectItResult<THarness, TProvided> {
|
|
50
|
+
readonly effectApp: EffectTester<THarness, TProvided>;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export interface EffectItTest<R, TArgs extends readonly unknown[] = [context: TestContext]> {
|
package/src/vitest.ts
CHANGED
|
@@ -3,28 +3,32 @@ import { Cause, type Context, Effect, Exit, Layer, Scope } from "effect";
|
|
|
3
3
|
import * as TestClock from "effect/testing/TestClock";
|
|
4
4
|
import * as TestConsole from "effect/testing/TestConsole";
|
|
5
5
|
import { type AnyTestLayer, buildTestLayer } from "./any-test-layer.ts";
|
|
6
|
-
import type { EffectClock, EffectTest, EffectTester, EffectTestOptions, MakeEffectItOptions, MakeEffectItResult } from "./types.ts";
|
|
6
|
+
import type { EffectClock, EffectTest, EffectTestBody, EffectTester, EffectTestOptions, MakeEffectItOptions, MakeEffectItResult } from "./types.ts";
|
|
7
7
|
|
|
8
8
|
const fixtureName = "__effectTestContext";
|
|
9
9
|
|
|
10
|
-
type EffectFixture<
|
|
11
|
-
readonly [fixtureName]: Context.Context<
|
|
10
|
+
type EffectFixture<TProvided> = TestContext & {
|
|
11
|
+
readonly [fixtureName]: Context.Context<TProvided>;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
type FixtureTest<
|
|
14
|
+
type FixtureTest<TProvided> = (name: string, options: TestOptions, body: (context: EffectFixture<TProvided>) => Promise<unknown>) => void;
|
|
15
15
|
|
|
16
16
|
const TestClockEnvironment = Layer.mergeAll(TestConsole.layer, TestClock.layer());
|
|
17
17
|
const LiveClockEnvironment = TestConsole.layer;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
function environmentFor(clock: EffectClock) {
|
|
20
|
+
return clock === "live" ? LiveClockEnvironment : TestClockEnvironment;
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
fixture: Context.Context<
|
|
23
|
-
context: Omit<EffectFixture<
|
|
24
|
-
): EffectFixture<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
function restoreContext<TProvided>(
|
|
24
|
+
fixture: Context.Context<TProvided>,
|
|
25
|
+
context: Omit<EffectFixture<TProvided>, typeof fixtureName>,
|
|
26
|
+
): EffectFixture<TProvided> {
|
|
27
|
+
return {
|
|
28
|
+
...context,
|
|
29
|
+
[fixtureName]: fixture,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
28
32
|
|
|
29
33
|
const runEffectTest = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>, context: TestContext, clock: EffectClock): Promise<A> =>
|
|
30
34
|
Effect.runPromise(
|
|
@@ -40,12 +44,10 @@ const runEffectTest = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>, context:
|
|
|
40
44
|
{ signal: context.signal },
|
|
41
45
|
);
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
options: number | EffectTestOptions | undefined,
|
|
45
|
-
): {
|
|
47
|
+
function splitOptions(options: number | EffectTestOptions | undefined): {
|
|
46
48
|
readonly vitest: TestOptions;
|
|
47
49
|
readonly clock: EffectClock | undefined;
|
|
48
|
-
}
|
|
50
|
+
} {
|
|
49
51
|
if (typeof options === "number") {
|
|
50
52
|
return { clock: undefined, vitest: { timeout: options } };
|
|
51
53
|
}
|
|
@@ -54,10 +56,10 @@ const splitOptions = (
|
|
|
54
56
|
}
|
|
55
57
|
const { clock, ...vitest } = options;
|
|
56
58
|
return { clock, vitest };
|
|
57
|
-
}
|
|
59
|
+
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
effectIt.extend(fixtureName, { scope: "worker" }, async ({}, { onCleanup }) => {
|
|
61
|
+
function makeFixtureIt<TLayer extends AnyTestLayer>(layer: TLayer) {
|
|
62
|
+
return effectIt.extend(fixtureName, { scope: "worker" }, async ({}, { onCleanup }) => {
|
|
61
63
|
const scope = Effect.runSync(Scope.make());
|
|
62
64
|
onCleanup(() => Effect.runPromise(Scope.close(scope, Exit.void)));
|
|
63
65
|
|
|
@@ -68,29 +70,27 @@ const makeFixtureIt = <TLayer extends AnyTestLayer>(layer: TLayer) =>
|
|
|
68
70
|
throw error;
|
|
69
71
|
}
|
|
70
72
|
});
|
|
73
|
+
}
|
|
71
74
|
|
|
72
|
-
export const makeEffectIt = <
|
|
73
|
-
options: MakeEffectItOptions<
|
|
74
|
-
): MakeEffectItResult<
|
|
75
|
-
type Provided = Layer.Success<
|
|
75
|
+
export const makeEffectIt = <THarness, TTestLayer extends AnyTestLayer>(
|
|
76
|
+
options: MakeEffectItOptions<THarness, TTestLayer>,
|
|
77
|
+
): MakeEffectItResult<THarness, Layer.Success<TTestLayer>> => {
|
|
78
|
+
type Provided = Layer.Success<TTestLayer>;
|
|
76
79
|
const fixtureIt = makeFixtureIt(options.layer);
|
|
77
80
|
const defaultClock = options.clock ?? "test";
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
context: EffectFixture<Provided>,
|
|
82
|
-
): Effect.Effect<A, unknown, Scope.Scope> => {
|
|
83
|
-
const ready: Effect.Effect<A, unknown, Provided> = Effect.gen(function* () {
|
|
82
|
+
function run<A>(body: EffectTestBody<THarness, Provided, A>, context: EffectFixture<Provided>): Effect.Effect<A, unknown, Scope.Scope> {
|
|
83
|
+
const ready: Effect.Effect<A, unknown, Provided | Scope.Scope> = Effect.gen(function* () {
|
|
84
84
|
const harness = yield* options.makeHarness(context);
|
|
85
|
-
|
|
85
|
+
const result = body(harness, context);
|
|
86
|
+
return yield* Effect.isEffect(result) ? result : Effect.gen(() => result);
|
|
86
87
|
});
|
|
87
88
|
const wrapped = options.around?.(ready) ?? ready;
|
|
88
89
|
return wrapped.pipe(Effect.provide(context[fixtureName]));
|
|
89
|
-
}
|
|
90
|
+
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
(
|
|
93
|
-
(name, body, testOptions) => {
|
|
92
|
+
function register(current: FixtureTest<Provided>): EffectTest<THarness, Provided> {
|
|
93
|
+
return (name, body, testOptions) => {
|
|
94
94
|
const split = splitOptions(testOptions);
|
|
95
95
|
const clock = split.clock ?? defaultClock;
|
|
96
96
|
current(name, split.vitest, ({ __effectTestContext, task, signal, onTestFailed, onTestFinished, skip, annotate, expect, _local }) => {
|
|
@@ -107,13 +107,14 @@ export const makeEffectIt = <Harness, TestLayer extends AnyTestLayer>(
|
|
|
107
107
|
return runEffectTest(run(body, context), context, clock);
|
|
108
108
|
});
|
|
109
109
|
};
|
|
110
|
+
}
|
|
110
111
|
|
|
111
|
-
const effectApp: EffectTester<
|
|
112
|
+
const effectApp: EffectTester<THarness, Provided> = Object.assign(register(fixtureIt), {
|
|
112
113
|
each:
|
|
113
|
-
<
|
|
114
|
-
<A,
|
|
114
|
+
<TItem>(cases: readonly TItem[]) =>
|
|
115
|
+
<A, TEff extends Effect.Effect<unknown, unknown, Provided | Scope.Scope>>(
|
|
115
116
|
name: string,
|
|
116
|
-
body: (item:
|
|
117
|
+
body: (item: TItem, harness: THarness, context: TestContext) => Generator<TEff, A, never> | Effect.Effect<A, unknown, Provided | Scope.Scope>,
|
|
117
118
|
testOptions?: number | EffectTestOptions,
|
|
118
119
|
) => {
|
|
119
120
|
const split = splitOptions(testOptions);
|