@shivaedev/test 0.0.0-stage → 0.9.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -2
  4. package/dist/anyTestLayer.d.ts +4 -0
  5. package/dist/anyTestLayer.d.ts.map +1 -0
  6. package/dist/anyTestLayer.js +5 -0
  7. package/dist/anyTestLayer.js.map +1 -0
  8. package/dist/eventually.d.ts +9 -0
  9. package/dist/eventually.d.ts.map +1 -0
  10. package/dist/eventually.js +25 -0
  11. package/dist/eventually.js.map +1 -0
  12. package/dist/internal/environmentFor.d.ts +5 -0
  13. package/dist/internal/environmentFor.d.ts.map +1 -0
  14. package/dist/internal/environmentFor.js +16 -0
  15. package/dist/internal/environmentFor.js.map +1 -0
  16. package/dist/internal/runEffectTest.d.ts +5 -0
  17. package/dist/internal/runEffectTest.d.ts.map +1 -0
  18. package/dist/internal/runEffectTest.js +18 -0
  19. package/dist/internal/runEffectTest.js.map +1 -0
  20. package/dist/it.d.ts +3 -0
  21. package/dist/it.d.ts.map +1 -0
  22. package/dist/it.js +31 -0
  23. package/dist/it.js.map +1 -0
  24. package/dist/makeEffectIt.d.ts +5 -0
  25. package/dist/makeEffectIt.d.ts.map +1 -0
  26. package/dist/makeEffectIt.js +92 -0
  27. package/dist/makeEffectIt.js.map +1 -0
  28. package/dist/types.d.ts +44 -0
  29. package/dist/types.d.ts.map +1 -0
  30. package/dist/types.js +2 -0
  31. package/dist/types.js.map +1 -0
  32. package/dist/withLive.d.ts +3 -0
  33. package/dist/withLive.d.ts.map +1 -0
  34. package/dist/withLive.js +6 -0
  35. package/dist/withLive.js.map +1 -0
  36. package/package.json +73 -3
  37. package/src/anyTestLayer.ts +12 -0
  38. package/src/eventually.ts +35 -0
  39. package/src/internal/environmentFor.ts +24 -0
  40. package/src/internal/runEffectTest.ts +26 -0
  41. package/src/it.ts +45 -0
  42. package/src/makeEffectIt.ts +126 -0
  43. package/src/types.ts +74 -0
  44. package/src/withLive.ts +6 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,140 @@
1
+ # Changelog
2
+
3
+ ## 0.9.0 - 2026-10-25
4
+
5
+ ### Added
6
+
7
+ - `@shivaedev/effect-test` readiness retries accept `timeout` and `wait` options. A timeout preserves the final typed failure; a custom wait lets adapters settle observable work through the same retry engine.
8
+
9
+ ### Breaking
10
+
11
+ - `@shivaedev/effect-react/client/testing.ts` now returns an Effect from `eventually(assert)`. Replace `yield* Effect.promise(() => eventually(assert))` with `yield* eventually(assert)` in stories and Effect tests, or replace `await eventually(assert)` with `await Effect.runPromise(eventually(assert))` in async tests. Assertions wait inside React `act` in live time, including under TestClock. Timeout errors include elapsed milliseconds and the last assertion message, with the assertion error as their `cause`.
12
+
13
+ ## 0.8.1 - 2026-10-25
14
+
15
+ ### Fixed
16
+
17
+ - Close shared test service Layers before their test file completes so database cleanup finishes before Vitest starts terminating the fork.
18
+
19
+ ## 0.8.0 - 2026-10-25
20
+
21
+ ### Breaking
22
+
23
+ - Move `@shivaedev/effect-test/vitest.ts` to `@shivaedev/effect-test/makeEffectIt.ts`. Change imports to `@shivaedev/effect-test/makeEffectIt.ts`; the exported `makeEffectIt` function is unchanged.
24
+
25
+ ## 0.7.2 - 2026-10-24
26
+
27
+ ### Changed
28
+
29
+ - Require Effect and its companion packages at exactly 4.0.5. Generated applications use the same versions.
30
+
31
+ ## 0.7.1 - 2026-10-18
32
+
33
+ ### Fixed
34
+
35
+ - Wait for an interrupted test's Scope to close after its Vitest timeout, so application workers and database pools finish teardown before the next test starts. Keep the timeout as the test failure without leaking the interrupted program's rejection as an uncaught error.
36
+
37
+ ## 0.7.0 - 2026-10-18
38
+
39
+ ### Added
40
+
41
+ - Pass a UTC `DateTime` to the existing `clock` option to pin a story or Effect fixture before its per-test setup runs. The exported `EffectClock` type now accepts `DateTime.Utc` alongside `"test"` and `"live"`. Hosts, workers and screens acquired with the story's Effect Context share native `Clock` / `TestClock`, including explicit `TestClock.adjust` advances; application code using `Clock` or `DateTime.now` needs no changes.
42
+
43
+ ## 0.6.0 - 2026-10-18
44
+
45
+ ### Breaking
46
+
47
+ - Modules are named in camelCase: `any-test-layer.ts` becomes `anyTestLayer.ts`.
48
+
49
+ ## 0.5.0 - 2026-10-16
50
+
51
+ ### Added
52
+
53
+ - `withLive(effect)` from `@shivaedev/effect-test/withLive.ts` runs sleeps and readiness polls in live time under either TestClock or the live clock, without advancing the surrounding TestClock.
54
+
55
+ ## 0.4.0 - 2026-10-13
56
+
57
+ ### Breaking
58
+
59
+ - Requires stable Effect 4.0.4 in place of 4.0.0-rc.112. Effect 4.0.4 moves the former `effect/unstable/*` modules to `effect/*` and renames constructors such as `Config.string` to `Config.String`. Install `@effect/vitest` 4.0.4 alongside it, which requires Vitest 5.
60
+
61
+ ## 0.3.2 - 2026-10-12
62
+
63
+ ### Added
64
+
65
+ - `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.
66
+ - Export `EffectTestBody<THarness, TProvided, A>` from `types.ts` to describe both fixture body forms.
67
+ - `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.
68
+
69
+ ### Changed
70
+
71
+ - Name descriptive generic type parameters with a `T` prefix, preserving test runner types and behavior.
72
+ - Verify conditional skipping in an isolated Vitest fixture, with passing suite assertions for registration and body non-execution instead of a skipped suite test.
73
+ - 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.
74
+ - Build package JavaScript, declarations and source maps with TypeScript 7.
75
+
76
+ ## 0.3.1 - 2026-10-06
77
+
78
+ ### Fixed
79
+
80
+ - `makeEffectIt` takes a Layer that provides nothing, such as `Layer.empty`, so a suite whose tests need no services passes no placeholder service. A test body that yields a service such a Layer does not provide still does not type-check.
81
+ - `@shivaedev/effect-test/any-test-layer.ts` exports `buildTestLayer(layer, scope)`, which builds any test Layer, `Layer.empty` included, into the services it provides.
82
+
83
+ ## 0.3.0 - 2026-10-05
84
+
85
+ ### Added
86
+
87
+ - `@shivaedev/effect-test/it.ts` exports `it`: the `it` of `@effect/vitest`, whose `effect` and `live` testers also take a generator function and run it with `Effect.gen`, so a test reads `it.effect("loads the item", function* () { … })`. They take a function that returns an Effect as well, as `@effect/vitest`'s testers do, so a file switches its import without rewriting other tests. Each tester supports `each`, `fails`, `only`, `runIf`, `skip` and `skipIf`, runs the body in a Scope, and types the yielded values, failures and requirements: a body that needs a service the test does not provide does not type-check. `effect` installs the test services, such as TestClock; `live` uses the live ones. The types are `EffectIt`, `EffectItTest` and `EffectItTester` in `@shivaedev/effect-test/types.ts`.
88
+
89
+ ## 0.2.0 - 2026-10-04
90
+
91
+ ### Changed
92
+
93
+ - Breaking: no root entry and no module that re-exports another. `exports` maps one `"./*.ts"` pattern to every module (`src` under the `source` condition, `dist` otherwise), so you import the module that defines a name, and a bundler sees only the modules you use. Where each name now lives:
94
+ - `@shivaedev/effect-test`: `@shivaedev/effect-test/eventually.ts` (`EventuallyOptions`, `eventually`), `@shivaedev/effect-test/types.ts` (`EffectClock`, `EffectTest`, `EffectTester`, `EffectTestOptions`, `MakeEffectItOptions`, `MakeEffectItResult`), `@shivaedev/effect-test/vitest.ts` (`makeEffectIt`)
95
+
96
+ ## 0.1.3 - 2026-10-04
97
+
98
+ ### Changed
99
+
100
+ - Imports that leave their folder go through `#` aliases declared in the `imports` field of `package.json`: `#*.ts` resolves to `src` under the `source` condition and to `dist` otherwise, and `#test/*` names a test file. The published modules and declarations name the package's own files through these aliases.
101
+
102
+ - Consolidate duplicate test cases around observable package behavior.
103
+
104
+ - Validate packed consumers and executable bins through the shared workspace gate.
105
+
106
+ - Build with the `@shivaedev/quality` tsconfig presets, which target ESNext and allow only erasable TypeScript syntax.
107
+
108
+ - Type-check with TypeScript 7 only. TypeScript 6 still builds the package.
109
+
110
+ - Lint and format with the `@shivaedev/quality` Biome preset through `quality lint` at the repository root, which replaces the package's `check` script.
111
+
112
+ - Run tests through the `@shivaedev/quality` Vitest projects, which take a test's environment from its file name. Type tests are named `*.typecheck.test.ts`.
113
+
114
+ - Keep `package.json` in sort-package-json key order, checked by `quality lint`.
115
+
116
+ ## 0.1.2 - 2026-09-26
117
+
118
+ ### Fixed
119
+
120
+ - Make `eventually` retry only typed failures under TestClock, matching the live
121
+ clock. Defects and interruption propagate without retrying.
122
+
123
+ ### Changed
124
+
125
+ - Document the live worker-Layer clock and the scope of per-test clock overrides.
126
+
127
+ ## 0.1.1 - 2026-09-04
128
+
129
+ ### Changed
130
+
131
+ - Require Effect 4.0.0-rc.112, so applications tracking the current release
132
+ candidate install without unmet peer warnings.
133
+
134
+ ## 0.1.0 - 2026-08-30
135
+
136
+ ### Added
137
+
138
+ - Add `makeEffectIt` for generator Effect tests on a worker-scoped Layer, with
139
+ TestClock by default, an optional `around` hook, and a Clock-aware
140
+ `eventually` helper.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShivaeDev
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 CHANGED
@@ -1,3 +1,243 @@
1
- # Temporary Holding Version
1
+ # @shivaedev/test
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
4
+
5
+ ## Why you want this
6
+
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:
8
+
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/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
+ });
20
+ ```
21
+
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.
29
+
30
+ A **Layer** describes how to acquire the fixture's services. `makeEffectIt` builds it in a Vitest file 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 file owns the acquired Layer and its scope; its finalizers finish before Vitest reports the file complete. 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 file 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
40
+
41
+ ```ts
42
+ import { expect } from "@effect/vitest";
43
+ import { Clock, Context, Effect, Layer } from "effect";
44
+ import { makeEffectIt } from "@shivaedev/test/makeEffectIt.ts";
45
+
46
+ class Token extends Context.Service<Token, string>()("test/Token") {}
47
+
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
+ });
63
+ ```
64
+
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.
66
+
67
+ A fixture that needs no services uses `Layer.empty`. This separate module replaces the token fixture above:
68
+
69
+ ```ts
70
+ import { expect } from "@effect/vitest";
71
+ import { Effect, Layer } from "effect";
72
+ import { makeEffectIt } from "@shivaedev/test/makeEffectIt.ts";
73
+
74
+ const { effectApp } = makeEffectIt({
75
+ layer: Layer.empty,
76
+ makeHarness: () => Effect.succeed({ shelf: 0 }),
77
+ });
78
+
79
+ effectApp("uses a harness without services", function* (harness) {
80
+ expect(harness.shelf).toBe(0);
81
+ });
82
+ ```
83
+
84
+ 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.
85
+
86
+ #### Wrapping the harness and body
87
+
88
+ This alternative token fixture adds a tracing wrapper:
89
+
90
+ ```ts
91
+ import { expect } from "@effect/vitest";
92
+ import { Context, Effect, Layer } from "effect";
93
+ import { makeEffectIt } from "@shivaedev/test/makeEffectIt.ts";
94
+
95
+ class Token extends Context.Service<Token, string>()("test/Token") {}
96
+
97
+ const { effectApp } = makeEffectIt({
98
+ around: (effect) => Effect.withSpan(effect, "test.token"),
99
+ layer: Layer.succeed(Token, "from-layer"),
100
+ makeHarness: () => Effect.service(Token),
101
+ });
102
+
103
+ effectApp("reads the harness", function* (token) {
104
+ expect(token).toBe("from-layer");
105
+ });
106
+ ```
107
+
108
+ `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`.
109
+
110
+ `@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 file 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/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.
111
+
112
+ ### 2. In every test: the generator
113
+
114
+ ```ts
115
+ import { expect } from "@effect/vitest";
116
+ import { Effect } from "effect";
117
+ import { it } from "@shivaedev/test/it.ts";
118
+
119
+ it.effect("reads an Effect result", function* ({ task }) {
120
+ expect(task.name).toBe("reads an Effect result");
121
+ expect(yield* Effect.succeed(3)).toBe(3);
122
+ });
123
+
124
+ it.effect("accepts an Effect-returning body", () =>
125
+ Effect.sync(() => expect(3).toBe(3)),
126
+ );
127
+
128
+ it.effect.each([1, 2])("passes the case %s", function* (count) {
129
+ expect(yield* Effect.succeed(count)).toBe(count);
130
+ });
131
+ ```
132
+
133
+ 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.
134
+
135
+ For a fixture's `effectApp.each`, the arguments are the case, harness and context:
136
+
137
+ ```ts
138
+ effectApp.each(["alpha", "beta"])("checks %s", function* (item, harness, context) {
139
+ expect(context.task.name).toContain(item);
140
+ expect(harness.token).toBe("from-layer");
141
+ });
142
+ ```
143
+
144
+ This uses the token fixture from the first example. Case and harness types are inferred. 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.
145
+
146
+ ### Choosing which clock runs the test
147
+
148
+ `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:
149
+
150
+ ```ts
151
+ effectApp("reads live time", function* () {
152
+ expect(yield* Clock.currentTimeMillis).toBeGreaterThan(1_000_000);
153
+ }, { clock: "live" });
154
+ ```
155
+
156
+ The override changes the per-test environment used by the wrapper, harness and body. A UTC `DateTime` also works as `clock`: it selects native TestClock and sets its initial instant before the wrapper or harness runs. `TestClock.adjust` advances that clock explicitly. For example, `{ clock: DateTime.makeUnsafe("2026-10-17T12:34:56.000Z") }` pins time to that UTC instant; import `DateTime` from `effect`. The same option is available on a fixture's factory.
157
+
158
+ 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.
159
+
160
+ A `Clock.Clock` supplied by the fixture's Layer takes precedence over the runner's default Clock in the wrapper, harness and body. Reads and sleeps use that fixture Clock, including explicit advancement when it is a native TestClock.
161
+
162
+ Both test and live clock fixtures install a fresh native TestConsole for each test. `Console.log` from `effect` is captured by `TestConsole.logLines` from `effect/testing/TestConsole`; one test's output does not carry into the next.
163
+
164
+ ### Waiting for a typed readiness condition
165
+
166
+ `eventually(effect, options)` retries the Effect's typed failures until it succeeds or uses its retry budget. It uses the active Clock: under TestClock it advances time with `TestClock.adjust`; under live time it waits with `Effect.sleep`.
167
+
168
+ ```ts
169
+ import { eventually } from "@shivaedev/test/eventually.ts";
170
+
171
+ effectApp("reaches the selected time", function* () {
172
+ const readyAt = yield* eventually(
173
+ Effect.gen(function* () {
174
+ const now = yield* Clock.currentTimeMillis;
175
+ return now >= 50 ? now : yield* Effect.fail("too-early");
176
+ }),
177
+ { interval: "7 millis", times: 8 },
178
+ );
179
+
180
+ expect(readyAt).toBe(56);
181
+ });
182
+ ```
183
+
184
+ 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. `timeout` sets an elapsed-time budget on the active clock, checked after a failed attempt. `wait` replaces the interval delay with a settlement Effect, so adapters such as React testing can own how pending work settles.
185
+
186
+ 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:
187
+
188
+ ```ts
189
+ effectApp("retries a readiness assertion", function* () {
190
+ let attempts = 0;
191
+
192
+ yield* eventually(
193
+ Effect.try(() => {
194
+ attempts += 1;
195
+ expect(attempts).toBe(3);
196
+ }),
197
+ { interval: "1 millis", times: 3 },
198
+ );
199
+
200
+ expect(attempts).toBe(3);
201
+ });
202
+ ```
203
+
204
+ This distinction applies under both clocks. The helper preserves the Effect's success, error and service types.
205
+
206
+ ### Waiting in live time without advancing TestClock
207
+
208
+ Use `withLive(effect)` from `@shivaedev/test/withLive.ts` for a real-time sleep or readiness poll in a harness, hook or story step. Under TestClock it delegates to native `TestClock.withLive`; under a live clock it runs the Effect directly. The surrounding clock is restored afterward, so waiting for external work does not move virtual token expiry.
209
+
210
+ ```ts
211
+ import { withLive } from "@shivaedev/test/withLive.ts";
212
+
213
+ yield* withLive(Effect.sleep("10 millis"));
214
+ yield* withLive(eventually(queueIsEmpty, { interval: "10 millis", times: 100 }));
215
+ ```
216
+
217
+ The entire wrapped Effect, including readiness checks and retry delays, uses live time. The same call works when a kit defaults to a live clock and one story opts into TestClock. Success, typed failure and service requirements are preserved.
218
+
219
+ ### API
220
+
221
+ Import the module that defines the name; the package has no root entry.
222
+
223
+ | Module | Exports |
224
+ | --- | --- |
225
+ | `@shivaedev/test/it.ts` | `it`, with generator-aware `effect` and `live` testers. |
226
+ | `@shivaedev/test/makeEffectIt.ts` | `makeEffectIt({ layer, makeHarness, around?, clock? })`, returning `{ effectApp }`. |
227
+ | `@shivaedev/test/eventually.ts` | `eventually(effect, options?)` and `EventuallyOptions`. |
228
+ | `@shivaedev/test/withLive.ts` | `withLive(effect)`, a live-time region under either clock. |
229
+ | `@shivaedev/test/anyTestLayer.ts` | `AnyTestLayer` and `buildTestLayer(layer, scope)`, an Effect that builds the Layer's service Context in the supplied scope. |
230
+ | `@shivaedev/test/types.ts` | `EffectClock`, `EffectTestOptions`, `EffectTestBody`, `EffectTest`, `EffectTester`, `MakeEffectItOptions`, `MakeEffectItResult`, `EffectItTest`, `EffectItTester` and `EffectIt`. |
231
+
232
+ `eventually` waits `"10 millis"` between attempts by default; omitting `times` leaves retries unbounded.
233
+
234
+ ### Install, setup and limits
235
+
236
+ ```sh
237
+ pnpm add --save-dev @shivaedev/test @effect/vitest@4.0.5 effect@4.0.5 vitest@5.0.3
238
+ ```
239
+
240
+ 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`.
241
+
242
+ - 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.
243
+ - An unbounded readiness poll can run until Vitest ends the test. Set a retry budget when the condition may never become true.
@@ -0,0 +1,4 @@
1
+ import { type Context, type Effect, Layer, type Scope } from "effect";
2
+ export type AnyTestLayer = Layer.Layer<any, any, never> | Layer.Layer<never, any, never>;
3
+ export declare function buildTestLayer<TLayer extends AnyTestLayer>(layer: TLayer, scope: Scope.Scope): Effect.Effect<Context.Context<Layer.Success<TLayer>>, unknown>;
4
+ //# sourceMappingURL=anyTestLayer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anyTestLayer.d.ts","sourceRoot":"","sources":["../src/anyTestLayer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEtE,MAAM,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAGzF,wBAAgB,cAAc,CAAC,MAAM,SAAS,YAAY,EACzD,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,KAAK,CAAC,KAAK,GAChB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { Layer } from "effect";
2
+ export function buildTestLayer(layer, scope) {
3
+ return Layer.buildWithScope(layer, scope);
4
+ }
5
+ //# sourceMappingURL=anyTestLayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anyTestLayer.js","sourceRoot":"","sources":["../src/anyTestLayer.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,EAAc,MAAM,QAAQ,CAAC;AAStE,MAAM,UAAU,cAAc,CAAC,KAAyC,EAAE,KAAkB;IAC3F,OAAO,KAAK,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Duration, Effect } from "effect";
2
+ export interface EventuallyOptions {
3
+ readonly interval?: Duration.Input;
4
+ readonly timeout?: Duration.Input;
5
+ readonly times?: number;
6
+ readonly wait?: Effect.Effect<void>;
7
+ }
8
+ export declare const eventually: <A, E, R>(effect: Effect.Effect<A, E, R>, options?: EventuallyOptions | undefined) => Effect.Effect<A, E, R>;
9
+ //# sourceMappingURL=eventually.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventually.d.ts","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,QAAQ,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAGzD,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,eAAO,MAAM,UAAU,GAAgC,CAAC,EAAE,CAAC,EAAE,CAAC,oGAwB5D,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { Clock, Duration, Effect, Result } from "effect";
2
+ import * as TestClock from "effect/testing/TestClock";
3
+ export const eventually = Effect.fnUntraced(function* (effect, options) {
4
+ const clock = yield* Clock.Clock;
5
+ const started = yield* Clock.currentTimeMillis;
6
+ const timeout = options?.timeout === undefined ? undefined : Duration.toMillis(Duration.fromInputUnsafe(options.timeout));
7
+ const interval = options?.interval ?? "10 millis";
8
+ const wait = options?.wait ?? ("adjust" in clock ? TestClock.adjust(interval) : Effect.sleep(interval));
9
+ let retriesLeft = options?.times;
10
+ for (;;) {
11
+ const result = yield* Effect.result(effect);
12
+ if (Result.isSuccess(result)) {
13
+ return result.success;
14
+ }
15
+ const elapsed = (yield* Clock.currentTimeMillis) - started;
16
+ if (retriesLeft === 0 || (timeout !== undefined && elapsed >= timeout)) {
17
+ return yield* Effect.fail(result.failure);
18
+ }
19
+ if (retriesLeft !== undefined) {
20
+ retriesLeft -= 1;
21
+ }
22
+ yield* wait;
23
+ }
24
+ });
25
+ //# sourceMappingURL=eventually.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventually.js","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AAStD,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACpD,MAA8B,EAC9B,OAA2B;IAE3B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IACjC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;IAC/C,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1H,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,WAAW,CAAC;IAClD,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxG,IAAI,WAAW,GAAG,OAAO,EAAE,KAAK,CAAC;IACjC,SAAS,CAAC;QACT,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9B,OAAO,MAAM,CAAC,OAAO,CAAC;QACvB,CAAC;QACD,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,GAAG,OAAO,CAAC;QAC3D,IAAI,WAAW,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,IAAI,OAAO,CAAC,EAAE,CAAC;YACxE,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC/B,WAAW,IAAI,CAAC,CAAC;QAClB,CAAC;QACD,KAAK,CAAC,CAAC,IAAI,CAAC;IACb,CAAC;AACF,CAAC,CAAC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { Layer } from "effect";
2
+ import * as TestConsole from "effect/testing/TestConsole";
3
+ import type { EffectClock } from "#types.ts";
4
+ export declare function environmentFor(clock: EffectClock): Layer.Layer<TestConsole.TestConsole, never, never>;
5
+ //# sourceMappingURL=environmentFor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environmentFor.d.ts","sourceRoot":"","sources":["../../src/internal/environmentFor.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,EAAE,MAAM,QAAQ,CAAC;AAExD,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAK7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,sDAehD"}
@@ -0,0 +1,16 @@
1
+ import { Clock, DateTime, Effect, Layer } from "effect";
2
+ import * as TestClock from "effect/testing/TestClock";
3
+ import * as TestConsole from "effect/testing/TestConsole";
4
+ const TestClockEnvironment = Layer.mergeAll(TestConsole.layer, TestClock.layer());
5
+ const LiveClockEnvironment = TestConsole.layer;
6
+ export function environmentFor(clock) {
7
+ if (typeof clock === "string") {
8
+ return clock === "live" ? LiveClockEnvironment : TestClockEnvironment;
9
+ }
10
+ return Layer.mergeAll(TestConsole.layer, Layer.effect(Clock.Clock, Effect.gen(function* () {
11
+ const testClock = yield* TestClock.make();
12
+ yield* testClock.setTime(DateTime.toEpochMillis(clock));
13
+ return testClock;
14
+ })));
15
+ }
16
+ //# sourceMappingURL=environmentFor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environmentFor.js","sourceRoot":"","sources":["../../src/internal/environmentFor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACxD,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAC;AAG1D,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,MAAM,UAAU,cAAc,CAAC,KAAkB;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,oBAAoB,CAAC;IACvE,CAAC;IACD,OAAO,KAAK,CAAC,QAAQ,CACpB,WAAW,CAAC,KAAK,EACjB,KAAK,CAAC,MAAM,CACX,KAAK,CAAC,KAAK,EACX,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QAC1C,KAAK,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;QACxD,OAAO,SAAS,CAAC;IAClB,CAAC,CAAC,CACF,CACD,CAAC;AACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { TestContext } from "@effect/vitest";
2
+ import { Effect, type Scope } from "effect";
3
+ import type { EffectClock } from "#types.ts";
4
+ export declare function runEffectTest<A, E>(effect: Effect.Effect<A, E, Scope.Scope>, context: TestContext, clock: EffectClock): Promise<A>;
5
+ //# sourceMappingURL=runEffectTest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runEffectTest.d.ts","sourceRoot":"","sources":["../../src/internal/runEffectTest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAS,MAAM,EAAQ,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG7C,wBAAgB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAoBlI"}
@@ -0,0 +1,18 @@
1
+ import { Cause, Effect, Exit } from "effect";
2
+ import { environmentFor } from "./environmentFor.js";
3
+ export function runEffectTest(effect, context, clock) {
4
+ const result = Effect.runPromise(Effect.gen(function* () {
5
+ const exit = yield* Effect.exit(effect);
6
+ if (Exit.isFailure(exit)) {
7
+ for (const error of Cause.prettyErrors(exit.cause)) {
8
+ yield* Effect.logError(error);
9
+ }
10
+ }
11
+ return yield* exit;
12
+ }).pipe(Effect.scoped, Effect.provide(environmentFor(clock))), { signal: context.signal });
13
+ // Vitest's timeout stops awaiting the body while Effect is still closing its Scope.
14
+ const settled = result.then(() => undefined, () => undefined);
15
+ context.onTestFinished(() => settled, 0);
16
+ return result;
17
+ }
18
+ //# sourceMappingURL=runEffectTest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runEffectTest.js","sourceRoot":"","sources":["../../src/internal/runEffectTest.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAc,MAAM,QAAQ,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,UAAU,aAAa,CAAO,MAAwC,EAAE,OAAoB,EAAE,KAAkB;IACrH,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAC/B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACpD,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC/B,CAAC;QACF,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,IAAI,CAAC;IACpB,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,EAC7D,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAC1B,CAAC;IACF,oFAAoF;IACpF,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAC1B,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CACf,CAAC;IACF,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACzC,OAAO,MAAM,CAAC;AACf,CAAC"}
package/dist/it.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { EffectIt } from "./types.ts";
2
+ export declare const it: EffectIt;
3
+ //# sourceMappingURL=it.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"it.d.ts","sourceRoot":"","sources":["../src/it.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAgC,MAAM,YAAY,CAAC;AAsCzE,eAAO,MAAM,EAAE,EAAE,QAGf,CAAC"}
package/dist/it.js ADDED
@@ -0,0 +1,31 @@
1
+ import { it as effectIt } from "@effect/vitest";
2
+ import { Effect } from "effect";
3
+ function effectOf(body) {
4
+ return (...args) => {
5
+ const result = body(...args);
6
+ return Effect.isEffect(result) ? result : Effect.gen(() => result);
7
+ };
8
+ }
9
+ function bodyTest(register) {
10
+ return (name, body, options) => register(name, effectOf(body), options);
11
+ }
12
+ function bodyTester(register) {
13
+ return Object.assign(bodyTest(register), {
14
+ each: (cases) => (name, body, options) => register.each(cases)(name, effectOf(body), options),
15
+ fails: bodyTest(register.fails),
16
+ only: bodyTest(register.only),
17
+ runIf: (condition) => bodyTest(register.runIf(condition)),
18
+ skip: bodyTest(register.skip),
19
+ skipIf: (condition) => bodyTest(register.skipIf(condition)),
20
+ });
21
+ }
22
+ const testers = {};
23
+ const vitestTests = new Proxy(effectIt, {
24
+ get: (target, key, receiver) => (Object.hasOwn(testers, key) ? testers[key] : Reflect.get(target, key, receiver)),
25
+ set: (_target, key, value) => Reflect.set(testers, key, value),
26
+ });
27
+ export const it = Object.assign(vitestTests, {
28
+ effect: bodyTester(effectIt.effect),
29
+ live: bodyTester(effectIt.live),
30
+ });
31
+ //# sourceMappingURL=it.js.map
package/dist/it.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"it.js","sourceRoot":"","sources":["../src/it.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,IAAI,QAAQ,EAAmD,MAAM,gBAAgB,CAAC;AACjG,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAQhC,SAAS,QAAQ,CAAsC,IAAoB;IAC1E,OAAO,CAAC,GAAG,IAAW,EAAsC,EAAE;QAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;IACpE,CAAC,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAI,QAAwB;IAC5C,OAAO,CAAC,IAAY,EAAE,IAAqC,EAAE,OAA8B,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;AACzI,CAAC;AAED,SAAS,UAAU,CAAI,QAA0B;IAChD,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;QACxC,IAAI,EACH,CAAQ,KAAuB,EAAkC,EAAE,CACnE,CAAC,IAAY,EAAE,IAA4B,EAAE,OAA8B,EAAE,EAAE,CAC9E,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QACrD,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC/B,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC7B,KAAK,EAAE,CAAC,SAAkB,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAClE,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC7B,MAAM,EAAE,CAAC,SAAkB,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;KACpE,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,GAAiC,EAAE,CAAC;AAEjD,MAAM,WAAW,GAAG,IAAI,KAAK,CAAU,QAAQ,EAAE;IAChD,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;IACjH,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC;CAC9D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,EAAE,GAAa,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE;IACtD,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC;IACnC,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC;CAC/B,CAAC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { type Layer } from "effect";
2
+ import { type AnyTestLayer } from "./anyTestLayer.ts";
3
+ import type { MakeEffectItOptions, MakeEffectItResult } from "./types.ts";
4
+ export declare function makeEffectIt<THarness, TTestLayer extends AnyTestLayer>(options: MakeEffectItOptions<THarness, TTestLayer>): MakeEffectItResult<THarness, Layer.Success<TTestLayer>>;
5
+ //# sourceMappingURL=makeEffectIt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"makeEffectIt.d.ts","sourceRoot":"","sources":["../src/makeEffectIt.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,KAAK,EAAS,MAAM,QAAQ,CAAC;AAEvE,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAA4E,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAgDpJ,wBAAgB,YAAY,CAAC,QAAQ,EAAE,UAAU,SAAS,YAAY,EACrE,OAAO,EAAE,mBAAmB,CAAC,QAAQ,EAAE,UAAU,CAAC,GAChD,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAuEzD"}
@@ -0,0 +1,92 @@
1
+ import { it as effectIt } from "@effect/vitest";
2
+ import { Effect, Exit, Scope } from "effect";
3
+ import { runEffectTest } from "#internal/runEffectTest.ts";
4
+ import { buildTestLayer } from "./anyTestLayer.js";
5
+ const FIXTURE_NAME = "__effectTestContext";
6
+ function restoreContext(fixture, context) {
7
+ return {
8
+ ...context,
9
+ [FIXTURE_NAME]: fixture,
10
+ };
11
+ }
12
+ function splitOptions(options) {
13
+ if (typeof options === "number") {
14
+ return { clock: undefined, vitest: { timeout: options } };
15
+ }
16
+ if (options === undefined) {
17
+ return { clock: undefined, vitest: {} };
18
+ }
19
+ const { clock, ...vitest } = options;
20
+ return { clock, vitest };
21
+ }
22
+ function makeFixtureIt(layer) {
23
+ return effectIt.extend(FIXTURE_NAME, { scope: "file" }, async ({}, { onCleanup }) => {
24
+ const scope = Effect.runSync(Scope.make());
25
+ onCleanup(() => Effect.runPromise(Scope.close(scope, Exit.void)));
26
+ try {
27
+ return await Effect.runPromise(buildTestLayer(layer, scope));
28
+ }
29
+ catch (error) {
30
+ await Effect.runPromise(Scope.close(scope, Exit.void));
31
+ throw error;
32
+ }
33
+ });
34
+ }
35
+ export function makeEffectIt(options) {
36
+ const fixtureIt = makeFixtureIt(options.layer);
37
+ const defaultClock = options.clock ?? "test";
38
+ function run(body, context) {
39
+ const ready = Effect.gen(function* () {
40
+ const harness = yield* options.makeHarness(context);
41
+ const result = body(harness, context);
42
+ return yield* Effect.isEffect(result) ? result : Effect.gen(() => result);
43
+ });
44
+ const wrapped = options.around?.(ready) ?? ready;
45
+ return wrapped.pipe(Effect.provide(context[FIXTURE_NAME]));
46
+ }
47
+ function register(current) {
48
+ return (name, body, testOptions) => {
49
+ const split = splitOptions(testOptions);
50
+ const clock = split.clock ?? defaultClock;
51
+ current(name, split.vitest, ({ __effectTestContext, task, signal, onTestFailed, onTestFinished, skip, annotate, expect, bench }) => {
52
+ const context = restoreContext(__effectTestContext, {
53
+ annotate,
54
+ bench,
55
+ expect,
56
+ onTestFailed,
57
+ onTestFinished,
58
+ signal,
59
+ skip,
60
+ task,
61
+ });
62
+ return runEffectTest(run(body, context), context, clock);
63
+ });
64
+ };
65
+ }
66
+ const effectApp = Object.assign(register(fixtureIt), {
67
+ each: (cases) => (name, body, testOptions) => {
68
+ const split = splitOptions(testOptions);
69
+ const clock = split.clock ?? defaultClock;
70
+ fixtureIt.for(cases)(name, split.vitest, (item, { __effectTestContext, task, signal, onTestFailed, onTestFinished, skip, annotate, expect, bench }) => {
71
+ const context = restoreContext(__effectTestContext, {
72
+ annotate,
73
+ bench,
74
+ expect,
75
+ onTestFailed,
76
+ onTestFinished,
77
+ signal,
78
+ skip,
79
+ task,
80
+ });
81
+ return runEffectTest(Effect.asVoid(run((harness, testContext) => body(item, harness, testContext), context)), context, clock);
82
+ });
83
+ },
84
+ fails: register(fixtureIt.fails),
85
+ only: register(fixtureIt.only),
86
+ runIf: (condition) => register(fixtureIt.runIf(condition)),
87
+ skip: register(fixtureIt.skip),
88
+ skipIf: (condition) => register(fixtureIt.skipIf(condition)),
89
+ });
90
+ return { effectApp };
91
+ }
92
+ //# sourceMappingURL=makeEffectIt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"makeEffectIt.js","sourceRoot":"","sources":["../src/makeEffectIt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,IAAI,QAAQ,EAAsC,MAAM,gBAAgB,CAAC;AACpF,OAAO,EAAgB,MAAM,EAAE,IAAI,EAAc,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAqB,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGtE,MAAM,YAAY,GAAG,qBAAqB,CAAC;AAQ3C,SAAS,cAAc,CACtB,OAAmC,EACnC,OAA4D;IAE5D,OAAO;QACN,GAAG,OAAO;QACV,CAAC,YAAY,CAAC,EAAE,OAAO;KACvB,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,OAA+C;IAIpE,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;IAC3D,CAAC;IACD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACzC,CAAC;IACD,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;IACrC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AAED,SAAS,aAAa,CAA8B,KAAa;IAChE,OAAO,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;QACnF,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3C,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAElE,IAAI,CAAC;YACJ,OAAO,MAAM,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAC9D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YACvD,MAAM,KAAK,CAAC;QACb,CAAC;IACF,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAC3B,OAAkD;IAGlD,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAAC;IAE7C,SAAS,GAAG,CAAI,IAA2C,EAAE,OAAgC;QAC5F,MAAM,KAAK,GAAsD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YACpF,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YACpD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACtC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;QAC3E,CAAC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;QACjD,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,SAAS,QAAQ,CAAC,OAA8B;QAC/C,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE;YAClC,MAAM,KAAK,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;YACxC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,YAAY,CAAC;YAC1C,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE;gBAClI,MAAM,OAAO,GAAG,cAAc,CAAC,mBAAmB,EAAE;oBACnD,QAAQ;oBACR,KAAK;oBACL,MAAM;oBACN,YAAY;oBACZ,cAAc;oBACd,MAAM;oBACN,IAAI;oBACJ,IAAI;iBACJ,CAAC,CAAC;gBACH,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;YAC1D,CAAC,CAAC,CAAC;QACJ,CAAC,CAAC;IACH,CAAC;IAED,MAAM,SAAS,GAAqC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE;QACtF,IAAI,EACH,CAAQ,KAAuB,EAAE,EAAE,CACnC,CACC,IAAY,EACZ,IAA6I,EAC7I,WAAwC,EACvC,EAAE;YACH,MAAM,KAAK,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;YACxC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,YAAY,CAAC;YAC1C,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CACnB,IAAI,EACJ,KAAK,CAAC,MAAM,EACZ,CAAC,IAAI,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE;gBAC5G,MAAM,OAAO,GAAG,cAAc,CAAC,mBAAmB,EAAE;oBACnD,QAAQ;oBACR,KAAK;oBACL,MAAM;oBACN,YAAY;oBACZ,cAAc;oBACd,MAAM;oBACN,IAAI;oBACJ,IAAI;iBACJ,CAAC,CAAC;gBACH,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,WAAW,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;YAC/H,CAAC,CACD,CAAC;QACH,CAAC;QACF,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC;QAChC,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC;QAC9B,KAAK,EAAE,CAAC,SAAkB,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACnE,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC;QAC9B,MAAM,EAAE,CAAC,SAAkB,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;KACrE,CAAC,CAAC;IAEH,OAAO,EAAE,SAAS,EAAE,CAAC;AACtB,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { TestContext, TestOptions } from "@effect/vitest";
2
+ import type { DateTime, Effect, Layer, Scope } from "effect";
3
+ import type { TestAPI } from "vitest";
4
+ import type { AnyTestLayer } from "./anyTestLayer.ts";
5
+ export type EffectClock = "test" | "live" | DateTime.Utc;
6
+ export type EffectTestOptions = TestOptions & {
7
+ readonly clock?: EffectClock;
8
+ };
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>;
18
+ }
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;
21
+ readonly clock?: EffectClock | undefined;
22
+ readonly layer: TTestLayer;
23
+ readonly makeHarness: (context: TestContext) => Effect.Effect<THarness, unknown, Layer.Success<TTestLayer> | Scope.Scope>;
24
+ }
25
+ export interface MakeEffectItResult<THarness, TProvided> {
26
+ readonly effectApp: EffectTester<THarness, TProvided>;
27
+ }
28
+ export interface EffectItTest<R, TArgs extends readonly unknown[] = [context: TestContext]> {
29
+ <TEffect extends Effect.Effect<unknown, unknown, R>, A>(name: string, body: (...args: TArgs) => Generator<TEffect, A, never>, options?: number | TestOptions): void;
30
+ <A, E>(name: string, body: (...args: TArgs) => Effect.Effect<A, E, R>, options?: number | TestOptions): void;
31
+ }
32
+ export interface EffectItTester<R> extends EffectItTest<R> {
33
+ readonly each: <TItem>(cases: readonly TItem[]) => EffectItTest<R, [item: TItem]>;
34
+ readonly fails: EffectItTest<R>;
35
+ readonly only: EffectItTest<R>;
36
+ readonly runIf: (condition: unknown) => EffectItTest<R>;
37
+ readonly skip: EffectItTest<R>;
38
+ readonly skipIf: (condition: unknown) => EffectItTest<R>;
39
+ }
40
+ export type EffectIt = TestAPI & {
41
+ readonly effect: EffectItTester<Scope.Scope>;
42
+ readonly live: EffectItTester<Scope.Scope>;
43
+ };
44
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ import { type Effect } from "effect";
2
+ export declare function withLive<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
3
+ //# sourceMappingURL=withLive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withLive.d.ts","sourceRoot":"","sources":["../src/withLive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAG5C,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAExF"}
@@ -0,0 +1,6 @@
1
+ import { Clock } from "effect";
2
+ import * as TestClock from "effect/testing/TestClock";
3
+ export function withLive(effect) {
4
+ return Clock.clockWith((clock) => ("withLive" in clock ? TestClock.withLive(effect) : effect));
5
+ }
6
+ //# sourceMappingURL=withLive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withLive.js","sourceRoot":"","sources":["../src/withLive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAe,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AAEtD,MAAM,UAAU,QAAQ,CAAU,MAA8B;IAC/D,OAAO,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,UAAU,IAAI,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAChG,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@shivaedev/test",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.9.0",
4
+ "description": "Generic Effect Vitest runner with file-scoped Layers and TestClock",
5
+ "keywords": [
6
+ "effect"
7
+ ],
8
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/test#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/ShivaeDev/platform/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ShivaeDev/platform.git",
15
+ "directory": "packages/test"
16
+ },
17
+ "license": "MIT",
18
+ "sideEffects": false,
19
+ "type": "module",
20
+ "imports": {
21
+ "#test/*": "./src/test-support/*",
22
+ "#*.ts": {
23
+ "source": "./src/*.ts",
24
+ "types": "./dist/*.d.ts",
25
+ "default": "./dist/*.js"
26
+ }
27
+ },
28
+ "exports": {
29
+ "./*.ts": {
30
+ "source": "./src/*.ts",
31
+ "types": "./dist/*.d.ts",
32
+ "default": "./dist/*.js"
33
+ },
34
+ "./internal/*": null,
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "!src/**/*.test.ts",
41
+ "!src/**/*.spec.ts",
42
+ "!src/**/test-support",
43
+ "CHANGELOG.md",
44
+ "README.md"
45
+ ],
46
+ "devDependencies": {
47
+ "@effect/vitest": "4.0.5",
48
+ "@shivaedev/quality": "0.41.1",
49
+ "@types/node": "24.19.2",
50
+ "effect": "4.0.5",
51
+ "typescript": "7.0.2",
52
+ "vitest": "5.0.5"
53
+ },
54
+ "peerDependencies": {
55
+ "@effect/vitest": "4.0.5",
56
+ "effect": "4.0.5"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@effect/vitest": {
60
+ "optional": true
61
+ }
62
+ },
63
+ "engines": {
64
+ "node": ">=24"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public",
68
+ "provenance": true
69
+ },
70
+ "scripts": {
71
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
72
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
73
+ "test": "vitest run",
74
+ "typecheck": "tsc --noEmit"
75
+ }
6
76
  }
@@ -0,0 +1,12 @@
1
+ import { type Context, type Effect, Layer, type Scope } from "effect";
2
+
3
+ export type AnyTestLayer = Layer.Layer<any, any, never> | Layer.Layer<never, any, never>;
4
+
5
+ // Only a Layer that provides never takes every test Layer, Layer.empty included, so the overload restores what the given Layer provides.
6
+ export function buildTestLayer<TLayer extends AnyTestLayer>(
7
+ layer: TLayer,
8
+ scope: Scope.Scope,
9
+ ): Effect.Effect<Context.Context<Layer.Success<TLayer>>, unknown>;
10
+ export function buildTestLayer(layer: Layer.Layer<never, unknown, never>, scope: Scope.Scope): Effect.Effect<Context.Context<never>, unknown> {
11
+ return Layer.buildWithScope(layer, scope);
12
+ }
@@ -0,0 +1,35 @@
1
+ import { Clock, Duration, Effect, Result } from "effect";
2
+ import * as TestClock from "effect/testing/TestClock";
3
+
4
+ export interface EventuallyOptions {
5
+ readonly interval?: Duration.Input;
6
+ readonly timeout?: Duration.Input;
7
+ readonly times?: number;
8
+ readonly wait?: Effect.Effect<void>;
9
+ }
10
+
11
+ export const eventually = Effect.fnUntraced(function* <A, E, R>(
12
+ effect: Effect.Effect<A, E, R>,
13
+ options?: EventuallyOptions,
14
+ ): Effect.fn.Return<A, E, R> {
15
+ const clock = yield* Clock.Clock;
16
+ const started = yield* Clock.currentTimeMillis;
17
+ const timeout = options?.timeout === undefined ? undefined : Duration.toMillis(Duration.fromInputUnsafe(options.timeout));
18
+ const interval = options?.interval ?? "10 millis";
19
+ const wait = options?.wait ?? ("adjust" in clock ? TestClock.adjust(interval) : Effect.sleep(interval));
20
+ let retriesLeft = options?.times;
21
+ for (;;) {
22
+ const result = yield* Effect.result(effect);
23
+ if (Result.isSuccess(result)) {
24
+ return result.success;
25
+ }
26
+ const elapsed = (yield* Clock.currentTimeMillis) - started;
27
+ if (retriesLeft === 0 || (timeout !== undefined && elapsed >= timeout)) {
28
+ return yield* Effect.fail(result.failure);
29
+ }
30
+ if (retriesLeft !== undefined) {
31
+ retriesLeft -= 1;
32
+ }
33
+ yield* wait;
34
+ }
35
+ });
@@ -0,0 +1,24 @@
1
+ import { Clock, DateTime, Effect, Layer } from "effect";
2
+ import * as TestClock from "effect/testing/TestClock";
3
+ import * as TestConsole from "effect/testing/TestConsole";
4
+ import type { EffectClock } from "#types.ts";
5
+
6
+ const TestClockEnvironment = Layer.mergeAll(TestConsole.layer, TestClock.layer());
7
+ const LiveClockEnvironment = TestConsole.layer;
8
+
9
+ export function environmentFor(clock: EffectClock) {
10
+ if (typeof clock === "string") {
11
+ return clock === "live" ? LiveClockEnvironment : TestClockEnvironment;
12
+ }
13
+ return Layer.mergeAll(
14
+ TestConsole.layer,
15
+ Layer.effect(
16
+ Clock.Clock,
17
+ Effect.gen(function* () {
18
+ const testClock = yield* TestClock.make();
19
+ yield* testClock.setTime(DateTime.toEpochMillis(clock));
20
+ return testClock;
21
+ }),
22
+ ),
23
+ );
24
+ }
@@ -0,0 +1,26 @@
1
+ import type { TestContext } from "@effect/vitest";
2
+ import { Cause, Effect, Exit, type Scope } from "effect";
3
+ import type { EffectClock } from "#types.ts";
4
+ import { environmentFor } from "./environmentFor.ts";
5
+
6
+ export function runEffectTest<A, E>(effect: Effect.Effect<A, E, Scope.Scope>, context: TestContext, clock: EffectClock): Promise<A> {
7
+ const result = Effect.runPromise(
8
+ Effect.gen(function* () {
9
+ const exit = yield* Effect.exit(effect);
10
+ if (Exit.isFailure(exit)) {
11
+ for (const error of Cause.prettyErrors(exit.cause)) {
12
+ yield* Effect.logError(error);
13
+ }
14
+ }
15
+ return yield* exit;
16
+ }).pipe(Effect.scoped, Effect.provide(environmentFor(clock))),
17
+ { signal: context.signal },
18
+ );
19
+ // Vitest's timeout stops awaiting the body while Effect is still closing its Scope.
20
+ const settled = result.then(
21
+ () => undefined,
22
+ () => undefined,
23
+ );
24
+ context.onTestFinished(() => settled, 0);
25
+ return result;
26
+ }
package/src/it.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { it as effectIt, type TestContext, type TestOptions, type Vitest } from "@effect/vitest";
2
+ import { Effect } from "effect";
3
+ import type { TestAPI } from "vitest";
4
+ import type { EffectIt, EffectItTest, EffectItTester } from "./types.ts";
5
+
6
+ type Body<TArgs extends readonly unknown[], R> = (
7
+ ...args: TArgs
8
+ ) => Effect.Effect<unknown, unknown, R> | Generator<Effect.Effect<unknown, unknown, R>, unknown, never>;
9
+
10
+ function effectOf<TArgs extends readonly unknown[], R>(body: Body<TArgs, R>) {
11
+ return (...args: TArgs): Effect.Effect<unknown, unknown, R> => {
12
+ const result = body(...args);
13
+ return Effect.isEffect(result) ? result : Effect.gen(() => result);
14
+ };
15
+ }
16
+
17
+ function bodyTest<R>(register: Vitest.Test<R>): EffectItTest<R> {
18
+ return (name: string, body: Body<[context: TestContext], R>, options?: number | TestOptions) => register(name, effectOf(body), options);
19
+ }
20
+
21
+ function bodyTester<R>(register: Vitest.Tester<R>): EffectItTester<R> {
22
+ return Object.assign(bodyTest(register), {
23
+ each:
24
+ <TItem>(cases: readonly TItem[]): EffectItTest<R, [item: TItem]> =>
25
+ (name: string, body: Body<[item: TItem], R>, options?: number | TestOptions) =>
26
+ register.each(cases)(name, effectOf(body), options),
27
+ fails: bodyTest(register.fails),
28
+ only: bodyTest(register.only),
29
+ runIf: (condition: unknown) => bodyTest(register.runIf(condition)),
30
+ skip: bodyTest(register.skip),
31
+ skipIf: (condition: unknown) => bodyTest(register.skipIf(condition)),
32
+ });
33
+ }
34
+
35
+ const testers: Record<PropertyKey, unknown> = {};
36
+
37
+ const vitestTests = new Proxy<TestAPI>(effectIt, {
38
+ get: (target, key, receiver) => (Object.hasOwn(testers, key) ? testers[key] : Reflect.get(target, key, receiver)),
39
+ set: (_target, key, value) => Reflect.set(testers, key, value),
40
+ });
41
+
42
+ export const it: EffectIt = Object.assign(vitestTests, {
43
+ effect: bodyTester(effectIt.effect),
44
+ live: bodyTester(effectIt.live),
45
+ });
@@ -0,0 +1,126 @@
1
+ import { it as effectIt, type TestContext, type TestOptions } from "@effect/vitest";
2
+ import { type Context, Effect, Exit, type Layer, Scope } from "effect";
3
+ import { runEffectTest } from "#internal/runEffectTest.ts";
4
+ import { type AnyTestLayer, buildTestLayer } from "./anyTestLayer.ts";
5
+ import type { EffectClock, EffectTest, EffectTestBody, EffectTester, EffectTestOptions, MakeEffectItOptions, MakeEffectItResult } from "./types.ts";
6
+
7
+ const FIXTURE_NAME = "__effectTestContext";
8
+
9
+ type EffectFixture<TProvided> = TestContext & {
10
+ readonly [FIXTURE_NAME]: Context.Context<TProvided>;
11
+ };
12
+
13
+ type FixtureTest<TProvided> = (name: string, options: TestOptions, body: (context: EffectFixture<TProvided>) => Promise<unknown>) => void;
14
+
15
+ function restoreContext<TProvided>(
16
+ fixture: Context.Context<TProvided>,
17
+ context: Omit<EffectFixture<TProvided>, typeof FIXTURE_NAME>,
18
+ ): EffectFixture<TProvided> {
19
+ return {
20
+ ...context,
21
+ [FIXTURE_NAME]: fixture,
22
+ };
23
+ }
24
+
25
+ function splitOptions(options: number | EffectTestOptions | undefined): {
26
+ readonly vitest: TestOptions;
27
+ readonly clock: EffectClock | undefined;
28
+ } {
29
+ if (typeof options === "number") {
30
+ return { clock: undefined, vitest: { timeout: options } };
31
+ }
32
+ if (options === undefined) {
33
+ return { clock: undefined, vitest: {} };
34
+ }
35
+ const { clock, ...vitest } = options;
36
+ return { clock, vitest };
37
+ }
38
+
39
+ function makeFixtureIt<TLayer extends AnyTestLayer>(layer: TLayer) {
40
+ return effectIt.extend(FIXTURE_NAME, { scope: "file" }, async ({}, { onCleanup }) => {
41
+ const scope = Effect.runSync(Scope.make());
42
+ onCleanup(() => Effect.runPromise(Scope.close(scope, Exit.void)));
43
+
44
+ try {
45
+ return await Effect.runPromise(buildTestLayer(layer, scope));
46
+ } catch (error) {
47
+ await Effect.runPromise(Scope.close(scope, Exit.void));
48
+ throw error;
49
+ }
50
+ });
51
+ }
52
+
53
+ export function makeEffectIt<THarness, TTestLayer extends AnyTestLayer>(
54
+ options: MakeEffectItOptions<THarness, TTestLayer>,
55
+ ): MakeEffectItResult<THarness, Layer.Success<TTestLayer>> {
56
+ type Provided = Layer.Success<TTestLayer>;
57
+ const fixtureIt = makeFixtureIt(options.layer);
58
+ const defaultClock = options.clock ?? "test";
59
+
60
+ function run<A>(body: EffectTestBody<THarness, Provided, A>, context: EffectFixture<Provided>): Effect.Effect<A, unknown, Scope.Scope> {
61
+ const ready: Effect.Effect<A, unknown, Provided | Scope.Scope> = Effect.gen(function* () {
62
+ const harness = yield* options.makeHarness(context);
63
+ const result = body(harness, context);
64
+ return yield* Effect.isEffect(result) ? result : Effect.gen(() => result);
65
+ });
66
+ const wrapped = options.around?.(ready) ?? ready;
67
+ return wrapped.pipe(Effect.provide(context[FIXTURE_NAME]));
68
+ }
69
+
70
+ function register(current: FixtureTest<Provided>): EffectTest<THarness, Provided> {
71
+ return (name, body, testOptions) => {
72
+ const split = splitOptions(testOptions);
73
+ const clock = split.clock ?? defaultClock;
74
+ current(name, split.vitest, ({ __effectTestContext, task, signal, onTestFailed, onTestFinished, skip, annotate, expect, bench }) => {
75
+ const context = restoreContext(__effectTestContext, {
76
+ annotate,
77
+ bench,
78
+ expect,
79
+ onTestFailed,
80
+ onTestFinished,
81
+ signal,
82
+ skip,
83
+ task,
84
+ });
85
+ return runEffectTest(run(body, context), context, clock);
86
+ });
87
+ };
88
+ }
89
+
90
+ const effectApp: EffectTester<THarness, Provided> = Object.assign(register(fixtureIt), {
91
+ each:
92
+ <TItem>(cases: readonly TItem[]) =>
93
+ <A, TEff extends Effect.Effect<unknown, unknown, Provided | Scope.Scope>>(
94
+ name: string,
95
+ body: (item: TItem, harness: THarness, context: TestContext) => Generator<TEff, A, never> | Effect.Effect<A, unknown, Provided | Scope.Scope>,
96
+ testOptions?: number | EffectTestOptions,
97
+ ) => {
98
+ const split = splitOptions(testOptions);
99
+ const clock = split.clock ?? defaultClock;
100
+ fixtureIt.for(cases)(
101
+ name,
102
+ split.vitest,
103
+ (item, { __effectTestContext, task, signal, onTestFailed, onTestFinished, skip, annotate, expect, bench }) => {
104
+ const context = restoreContext(__effectTestContext, {
105
+ annotate,
106
+ bench,
107
+ expect,
108
+ onTestFailed,
109
+ onTestFinished,
110
+ signal,
111
+ skip,
112
+ task,
113
+ });
114
+ return runEffectTest(Effect.asVoid(run((harness, testContext) => body(item, harness, testContext), context)), context, clock);
115
+ },
116
+ );
117
+ },
118
+ fails: register(fixtureIt.fails),
119
+ only: register(fixtureIt.only),
120
+ runIf: (condition: unknown) => register(fixtureIt.runIf(condition)),
121
+ skip: register(fixtureIt.skip),
122
+ skipIf: (condition: unknown) => register(fixtureIt.skipIf(condition)),
123
+ });
124
+
125
+ return { effectApp };
126
+ }
package/src/types.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { TestContext, TestOptions } from "@effect/vitest";
2
+ import type { DateTime, Effect, Layer, Scope } from "effect";
3
+ import type { TestAPI } from "vitest";
4
+ import type { AnyTestLayer } from "./anyTestLayer.ts";
5
+
6
+ export type EffectClock = "test" | "live" | DateTime.Utc;
7
+
8
+ export type EffectTestOptions = TestOptions & {
9
+ readonly clock?: EffectClock;
10
+ };
11
+
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>>(
18
+ name: string,
19
+ body: (harness: THarness, context: TestContext) => Generator<TEff, A, never> | Effect.Effect<A, unknown, TProvided | Scope.Scope>,
20
+ options?: number | EffectTestOptions,
21
+ ) => void;
22
+
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>>(
27
+ name: string,
28
+ body: (item: TItem, harness: THarness, context: TestContext) => Generator<TEff, A, never> | Effect.Effect<A, unknown, TProvided | Scope.Scope>,
29
+ options?: number | EffectTestOptions,
30
+ ) => void;
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>;
36
+ }
37
+
38
+ export interface MakeEffectItOptions<THarness, TTestLayer extends AnyTestLayer> {
39
+ readonly around?:
40
+ | (<A, E>(
41
+ effect: Effect.Effect<A, E, Layer.Success<TTestLayer> | Scope.Scope>,
42
+ ) => Effect.Effect<A, unknown, Layer.Success<TTestLayer> | Scope.Scope>)
43
+ | undefined;
44
+ readonly clock?: EffectClock | undefined;
45
+ readonly layer: TTestLayer;
46
+ readonly makeHarness: (context: TestContext) => Effect.Effect<THarness, unknown, Layer.Success<TTestLayer> | Scope.Scope>;
47
+ }
48
+
49
+ export interface MakeEffectItResult<THarness, TProvided> {
50
+ readonly effectApp: EffectTester<THarness, TProvided>;
51
+ }
52
+
53
+ export interface EffectItTest<R, TArgs extends readonly unknown[] = [context: TestContext]> {
54
+ <TEffect extends Effect.Effect<unknown, unknown, R>, A>(
55
+ name: string,
56
+ body: (...args: TArgs) => Generator<TEffect, A, never>,
57
+ options?: number | TestOptions,
58
+ ): void;
59
+ <A, E>(name: string, body: (...args: TArgs) => Effect.Effect<A, E, R>, options?: number | TestOptions): void;
60
+ }
61
+
62
+ export interface EffectItTester<R> extends EffectItTest<R> {
63
+ readonly each: <TItem>(cases: readonly TItem[]) => EffectItTest<R, [item: TItem]>;
64
+ readonly fails: EffectItTest<R>;
65
+ readonly only: EffectItTest<R>;
66
+ readonly runIf: (condition: unknown) => EffectItTest<R>;
67
+ readonly skip: EffectItTest<R>;
68
+ readonly skipIf: (condition: unknown) => EffectItTest<R>;
69
+ }
70
+
71
+ export type EffectIt = TestAPI & {
72
+ readonly effect: EffectItTester<Scope.Scope>;
73
+ readonly live: EffectItTester<Scope.Scope>;
74
+ };
@@ -0,0 +1,6 @@
1
+ import { Clock, type Effect } from "effect";
2
+ import * as TestClock from "effect/testing/TestClock";
3
+
4
+ export function withLive<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
5
+ return Clock.clockWith((clock) => ("withLive" in clock ? TestClock.withLive(effect) : effect));
6
+ }