@systemfsoftware/vitest 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +113 -61
- package/dist/TestClock.d.ts +1 -1
- package/dist/checks-DiuzHhQM.d.ts +232 -0
- package/dist/guard-BocOhXt2.mjs +246 -0
- package/dist/guard.d.ts +2 -0
- package/dist/guard.mjs +19 -0
- package/dist/index.d.ts +201 -287
- package/dist/index.mjs +353 -562
- package/dist/integration.d.ts +37 -0
- package/dist/integration.mjs +38 -0
- package/dist/test-context-hvSoKLtw.mjs +206 -0
- package/package.json +9 -9
- package/dist/refusals-BQtDszyU.mjs +0 -32
- package/dist/refusals-_6e3GzVK.d.ts +0 -37
- package/dist/refusals.d.ts +0 -3
- package/dist/refusals.mjs +0 -3
- package/dist/utils.d.ts +0 -197
- package/dist/utils.mjs +0 -227
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @systemfsoftware/vitest
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Tests take `expect` from the test callback and yield each check: `it(name, function* ({ expect }) { yield* expect(actual).toEqual(expected) })`. The package stops exporting `expect`, `assert` and the `utils` helpers. With its `guard` entry listed in `setupFiles`, an `expect` imported from Vitest, or a test registered through Vitest's own `it`, is refused. Also refused, each with its rewrite as the message: a second check on one observed state, an unyielded check, a body with no check, a non-generator body, `it.effect`/`it.scoped`/`it.scopedLive`, `beforeEach`/`afterEach`, and weak matchers such as `toHaveLength`, `toBeDefined`, snapshots, `expect.anything`, `expect.soft`/`expect.poll` and argument-less `toThrow`. Libraries that judge inside a test import `step`, `captureRunBinding` and the `Check`/`Expect`/`Asserted` types from the `integration` entry.
|
|
8
|
+
|
|
3
9
|
## 0.0.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @systemfsoftware/vitest
|
|
2
2
|
|
|
3
|
-
A fork of [`@effect/vitest`](https://github.com/Effect-TS/effect/tree/main/packages/vitest) whose defaults make the lazy test a good test. Write the obvious thing and you get a fresh build of your services, a second run that catches leaked state, virtual time,
|
|
3
|
+
A fork of [`@effect/vitest`](https://github.com/Effect-TS/effect/tree/main/packages/vitest) whose defaults make the lazy test a good test. `expect` is the parameter of the test's own body, every check is yielded, and one observed state gets one check. Write the obvious thing and you also get a fresh build of your services, a second run that catches leaked state, virtual time, and properties refuted against a constant impostor. Write the slop form and the refusal names the rewrite.
|
|
4
4
|
|
|
5
|
-
Everything upstream exports is still exported
|
|
5
|
+
Everything upstream exports is still exported except what the callback parameter replaces. `it`, `test`, `it.live`, `it.each`, `it.layer`, `layer`, `describe`, `it.prop`, `it.effect.prop`, `flakyTest`, `addEqualityTesters`, `makeMethods` and `describeWrapped` are here; `expect` and `assert` are not; `it.effect`, `it.scoped` and `it.scopedLive` are refusals carrying the generator rewrite; and the Vitest values the fork keeps are re-exported by name — the list is under [Compatibility](#compatibility-with-effectvitest). Two things differ from upstream: `describe` is the fork's lawful collector, and four accepted-input types are narrower. On top of that surface the fork adds `@systemfsoftware/vitest/integration` (`step`, `captureRunBinding`), `layer(L, { shared: true })`, a lawful `it.prop`, and `VitestTestContext` — the running test's context, published so a library can read it. `it`/`test`/`it.live`/`describeWrapped`/`layer`/`flakyTest` also take a data-last form (`it(body, timeout?)(name)`), so they pipe.
|
|
6
6
|
|
|
7
7
|
The defaults are forced, not opted into. Every package in this workspace imports `@systemfsoftware/vitest` by its own name, and published packages peer on it directly — nothing resolves through an alias to the upstream specifier.
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ npm install -D @systemfsoftware/vitest
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
|
-
import {
|
|
28
|
+
import { it, layer } from '@systemfsoftware/vitest'
|
|
29
29
|
import { Context, Effect, Layer, Ref } from 'effect'
|
|
30
30
|
|
|
31
31
|
class Store extends Context.Service<Store, Ref.Ref<ReadonlyArray<string>>>()('Store') {
|
|
@@ -33,16 +33,55 @@ class Store extends Context.Service<Store, Ref.Ref<ReadonlyArray<string>>>()('St
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
layer(Store.layer)('store', (it) => {
|
|
36
|
-
it
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
it('starts from an empty store', function*({ expect }) {
|
|
37
|
+
const store = yield* Store
|
|
38
|
+
const items = yield* Ref.get(store)
|
|
39
|
+
yield* expect(items).toEqual([])
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A test body is a generator, and the runner drives it: `yield*` a service, an Effect or a check. The layer is built fresh for the test, and the test runs twice on two different builds — a test that only passes the first time is a test that leaked state.
|
|
45
|
+
|
|
46
|
+
## Checks
|
|
47
|
+
|
|
48
|
+
`expect` is not importable. The only one in reach is the `{ expect }` the body receives, so a check is bound to its test by construction. Every matcher returns a **check**: an `Effect` that needs the runner's `Asserted` service, which is why it has to be yielded.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
it('ships a pending order', function*({ expect }) {
|
|
52
|
+
const order = yield* shop.place(items)
|
|
53
|
+
yield* expect(order).toMatchObject({ id: 1, status: 'Pending' })
|
|
42
54
|
})
|
|
43
55
|
```
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
- **One check per observed state.** A second check before the body's next non-check step — and a check inside a loop — is refused with `assert the state once` and the rewrite. A `yield*` of anything that is not a check opens a new observed state, so `yield* TestClock.adjust('3 seconds')` between two checks is a second state.
|
|
58
|
+
- **A check that fails stops the test there.** The generator is returned, so `finally` blocks and scope finalizers still run, and the report carries the diff.
|
|
59
|
+
- **A body that yields no check is refused** at the test name (compile time) and at run time; a check that is written but never yielded is refused as `written but never yielded`.
|
|
60
|
+
- **`toEqual` compares with Effect `Equal`**, so two structurally equal values of different references are equal.
|
|
61
|
+
- **The vocabulary is curated.** `toEqual`, `toStrictEqual`, `toBe`, `toContain`, `toMatch`, `toMatchObject`, `toThrow(X)`, `toSatisfy(fn, why)`, the call-ordering matchers, the ordered comparisons, and `objectContaining`/`arrayContaining`/`stringMatching`/`closeTo`/`any`/`schemaMatching` are kept. Everything weak is refused by name (see [Refusals](#refusals)), and a `boolean` actual is a type error — a boolean can only report `expected false to be true`.
|
|
62
|
+
|
|
63
|
+
### The guard
|
|
64
|
+
|
|
65
|
+
A check is the only way to assert, and the guard is what makes that true rather than advisory. `@systemfsoftware/vitest/guard` installs a wrapper over chai's assertion prototype and marks every task the fork registers. The shared Vitest config loads it into every test project, inline projects included, so no import is needed. A project stays out only when vitest-config's exemption table names it together with the foreign runner that registers its tests (oxlint's `RuleTester`, Storybook's plugin). A package outside that table that cannot resolve `@systemfsoftware/vitest/guard` fails at config load rather than running unguarded. Under it:
|
|
66
|
+
|
|
67
|
+
- **a raw `vitest` `expect` (or `assert`) is refused**, even beside a real check: "✗ an expect imported from vitest ran; take it from the test callback: it(name, function\* ({ expect }) { ... })";
|
|
68
|
+
- **a test registered with `vitest`'s own `it`/`test` is refused**: "✗ this test was registered with vitest's it; import it from @systemfsoftware/vitest".
|
|
69
|
+
|
|
70
|
+
## Lanes
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { describe, it, layer } from '@systemfsoftware/vitest'
|
|
74
|
+
|
|
75
|
+
it(name, function*({ expect }) { /* virtual time */ })
|
|
76
|
+
it.live(name, function*({ expect }) { /* the real clock */ })
|
|
77
|
+
it.each(rows)(name, function*(row, { expect }) { /* Vitest's it.for order */ })
|
|
78
|
+
layer(Service.layer)((it) => { it(name, function*({ expect }) { ... }) })
|
|
79
|
+
describe('a block', (it) => { ... })
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`it`, `test`, `it.live`, `it.each`, `it.only`, `it.skip`, `it.skipIf`, `it.runIf`, `it.fails`, `it.layer`, `layer` and `describeWrapped` all take a generator body. `it`/`test`/`it.live`/`describeWrapped`/`layer`/`flakyTest` also take a data-last form (`it(body, timeout?)(name)`), so they pipe. `it.prop`, `it.effect.prop`, `it.law` and `flakyTest` are unchanged from #512.
|
|
83
|
+
|
|
84
|
+
The habit lanes are refusals, not aliases: `it.effect`, `it.scoped` and `it.scopedLive` are compile-time errors and run-time throws carrying the generator rewrite. `it.effect.prop` still runs properties. `beforeEach` and `afterEach` are refused too — build what a test needs inside it; services come fresh per test from `layer`.
|
|
46
85
|
|
|
47
86
|
## Layers
|
|
48
87
|
|
|
@@ -60,60 +99,62 @@ That is how a module-level counter, a shared cache, or a process-global histogra
|
|
|
60
99
|
|
|
61
100
|
## Concurrency and shuffle
|
|
62
101
|
|
|
63
|
-
Tests inside a block run concurrently and in shuffled order, and each concurrent test keeps its own failure list and
|
|
102
|
+
Tests inside a block run concurrently and in shuffled order, and each concurrent test keeps its own failure list and check counts. Concurrency is what makes a cross-test leak reproducible: two tests that interleave are two tests whose state must not be shared. Nothing else pins their order.
|
|
64
103
|
|
|
65
104
|
A failure in a shuffled block names the seed the run used. Pass that seed back with `--sequence.seed=<seed>` and the same order replays, so an order-dependent failure stops being intermittent.
|
|
66
105
|
|
|
67
|
-
## Checks
|
|
68
|
-
|
|
69
|
-
`expect` records softly within one Effect step. Every failed check in the step is reported, and before any of the test's fibers takes its next step after a failed check, that fiber is interrupted — the step after a failed check never runs its side effects, which would only act on a state already known wrong. A child fiber is interrupted too. Anything thrown after a failed check is reported as `AfterFailedExpect`, most likely caused by the failure above it.
|
|
70
|
-
|
|
71
|
-
`toEqual` compares with Effect `Equal`, so two values that are structurally equal but different references are equal. A test that asserts nothing fails: the code under test returned something, so assert on it. A testing library whose checks are Effect-native can count them through `recordAssertion()`.
|
|
72
|
-
|
|
73
106
|
## Virtual time
|
|
74
107
|
|
|
75
|
-
|
|
108
|
+
Test bodies run on virtual time that advances only when the test's fibers are idle. `Effect.sleep("3 seconds")` returns without waiting; on a deadline tie the background sleepers wake before the test fiber; fractional-millisecond schedules work.
|
|
76
109
|
|
|
77
110
|
```ts
|
|
78
|
-
import {
|
|
111
|
+
import { it } from '@systemfsoftware/vitest'
|
|
79
112
|
import { Clock, Effect } from 'effect'
|
|
80
113
|
|
|
81
|
-
it
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}))
|
|
114
|
+
it('three seconds pass at once', function*({ expect }) {
|
|
115
|
+
const before = yield* Clock.currentTimeMillis
|
|
116
|
+
yield* Effect.sleep('3 seconds')
|
|
117
|
+
const after = yield* Clock.currentTimeMillis
|
|
118
|
+
yield* expect(after - before).toEqual(3000)
|
|
119
|
+
})
|
|
88
120
|
```
|
|
89
121
|
|
|
90
122
|
`TestClock.adjust` still moves the clock — on virtual time, adjusting is letting that much time pass, which is the one clock move a test can ask for. Effect v3 code that imports `TestClock` from `effect/TestClock` resolves to this fork's compat entry, `@systemfsoftware/vitest/TestClock`, through the shared Vitest config's `effect/TestClock` alias.
|
|
91
123
|
|
|
92
124
|
## Refusals
|
|
93
125
|
|
|
94
|
-
Each refusal states its rewrite, in the same words at compile time and at run time:
|
|
95
|
-
|
|
96
|
-
| Refused
|
|
97
|
-
|
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
Each refusal states its rewrite, in the same words at compile time (a `this` type, an argument union, or the test name) and at run time:
|
|
127
|
+
|
|
128
|
+
| Refused | The rewrite in the message |
|
|
129
|
+
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
130
|
+
| `toBeDefined`, `toBeTruthy`, `toBeFalsy`, `not.toBeNull`, `not.toBeUndefined` | Assert the value: `yield* expect(actual).toEqual(expected)` |
|
|
131
|
+
| `toHaveLength` | Assert the contents: `toEqual([...])`; only some of them: `toEqual(expect.arrayContaining([...]))` |
|
|
132
|
+
| `toHaveProperty` | Assert the shape once: `toMatchObject({ key: value, ... })` |
|
|
133
|
+
| `toBeInstanceOf`, `toBeTypeOf` | Assert the value: `toEqual(expected)`; only its shape: `toEqual(expect.schemaMatching(Schema))` |
|
|
134
|
+
| `toHaveBeenCalled`, `toHaveBeenCalledTimes` | `toHaveBeenCalledExactlyOnceWith(...args)`, or `expect(spy.mock.calls).toEqual([[...], [...]])` |
|
|
135
|
+
| every snapshot matcher | State the expected value: `toEqual(expected)`; for a throw: `toThrow(MyError)` |
|
|
136
|
+
| `expect.anything`, `.resolves`, `.rejects`, `expect.poll`, `expect.soft` | Name what the value must be, yield the Effect, or let virtual time pass |
|
|
137
|
+
| `toThrow()`, `toThrowError()`, `toSatisfy(fn)`, `toMatchObject({})` | Name the error, the reason, or the fields: `toThrow(MyError)`, `toSatisfy(predicate, "why")`, `toMatchObject({ status: "Pending" })` |
|
|
138
|
+
| a `boolean` actual | Pass the two values instead: `expect(a).toEqual(b)`; a predicate: `toSatisfy(predicate, "what must hold")` |
|
|
139
|
+
| a second check on one state | Assert the state once: `toMatchObject({...})`, `expect({ a, b }).toEqual({...})`, or `it.each` for several inputs |
|
|
140
|
+
| a check written but never yielded | Yield it: `yield* expect(actual).toEqual(expected)` |
|
|
141
|
+
| a body that yields no check | Yield one from the test's own `expect` |
|
|
142
|
+
| a sync body, an `async` body, a body returning an Effect | Pass the generator itself: `it(name, function* ({ expect }) { ... })` |
|
|
143
|
+
| `it.effect` / `it.scoped` / `it.scopedLive` | `it(name, function* ({ expect }) { const x = yield* program; yield* expect(x).toEqual(expected) })` |
|
|
144
|
+
| `beforeEach` / `afterEach` | Build what a test needs inside it; services come fresh per test from `layer(Service.layer)((it) => { ... })` |
|
|
145
|
+
| a body that needs a service nothing provides | Write the test inside `layer(Service.layer)((it) => { ... })`; every test gets its own fresh build |
|
|
146
|
+
| the positional `it.prop(name, [arbitraries], predicate)` | Name the function under test and pass a budget: `it.prop(name, { of, subject, runs }, holds)` |
|
|
147
|
+
|
|
148
|
+
A named refusal reads exactly like this:
|
|
149
|
+
|
|
150
|
+
> ✗ toHaveLength checks how many, not which. Assert the contents: toEqual([...]); only some of them: toEqual(expect.arrayContaining([...])).
|
|
110
151
|
|
|
111
152
|
## Properties
|
|
112
153
|
|
|
113
154
|
A property names the function under test. It runs `runs` times; omit `runs` and the run's configured default applies:
|
|
114
155
|
|
|
115
156
|
```ts
|
|
116
|
-
import {
|
|
157
|
+
import { it } from '@systemfsoftware/vitest'
|
|
117
158
|
import { Schema as S } from 'effect'
|
|
118
159
|
|
|
119
160
|
const sort = (xs: ReadonlyArray<number>): ReadonlyArray<number> => [...xs].sort((a, b) => a - b)
|
|
@@ -171,31 +212,38 @@ A class fails only when a sequential statistical test is confident its share is
|
|
|
171
212
|
|
|
172
213
|
## For testing libraries
|
|
173
214
|
|
|
174
|
-
A library
|
|
175
|
-
|
|
176
|
-
`owned(effect)` marks a region where `expect` throws instead of recording softly, so the enclosing Effect sees a failed check as a failure in its cause:
|
|
215
|
+
A library takes the test's `expect` by parameter and ends in a check:
|
|
177
216
|
|
|
178
217
|
```ts
|
|
179
|
-
import {
|
|
218
|
+
import type { Check, Expect } from '@systemfsoftware/vitest'
|
|
180
219
|
import { Effect } from 'effect'
|
|
181
220
|
|
|
182
|
-
|
|
183
|
-
|
|
221
|
+
export const runContract = (expect: Expect) =>
|
|
222
|
+
Effect.gen(function*() {
|
|
223
|
+
const report = yield* reportOf()
|
|
224
|
+
yield* expect(report).toMatchObject({ holds: true })
|
|
225
|
+
})
|
|
184
226
|
```
|
|
185
227
|
|
|
186
|
-
|
|
228
|
+
A flow that asserts at several points marks each point with `step` from `@systemfsoftware/vitest/integration`, so every phase opens its own observed state and each gets one check:
|
|
187
229
|
|
|
188
230
|
```ts
|
|
189
|
-
import {
|
|
231
|
+
import { step } from '@systemfsoftware/vitest/integration'
|
|
190
232
|
import { Effect } from 'effect'
|
|
191
233
|
|
|
192
|
-
export const
|
|
234
|
+
export const runPipeline = (expect: Expect) =>
|
|
235
|
+
Effect.gen(function*() {
|
|
236
|
+
yield* step(yield* phaseOne())
|
|
237
|
+
yield* expect(yield* phaseOne()).toMatchObject({ ok: true })
|
|
238
|
+
yield* step(yield* phaseTwo())
|
|
239
|
+
yield* expect(yield* phaseTwo()).toMatchObject({ ok: true })
|
|
240
|
+
})
|
|
193
241
|
```
|
|
194
242
|
|
|
195
|
-
`captureRunBinding` is the test's run binding. `bind`
|
|
243
|
+
`captureRunBinding` — from `@systemfsoftware/vitest/integration` — is the test's run binding. Capture it inside the test, then `bind` the effect before handing it to a runtime of your own — its own scheduler, a worker, a simulation kernel — so its checks count as that test's assertions. `bind` provides what a check reads off the test's own fiber (the ledger, and the running task context) and the bound effect no longer requires `Asserted`, which is what lets it cross into a runtime that cannot know the ledger. Every run of a bound effect opens its own observed state, so a runtime that re-runs it — a kernel's baseline and its seeded replays — checks each run once; two checks inside one run are still refused:
|
|
196
244
|
|
|
197
245
|
```ts
|
|
198
|
-
import { captureRunBinding } from '@systemfsoftware/vitest'
|
|
246
|
+
import { captureRunBinding } from '@systemfsoftware/vitest/integration'
|
|
199
247
|
import { Effect } from 'effect'
|
|
200
248
|
|
|
201
249
|
export const runOnOwnScheduler = <A, E>(
|
|
@@ -207,7 +255,7 @@ export const runOnOwnScheduler = <A, E>(
|
|
|
207
255
|
})
|
|
208
256
|
```
|
|
209
257
|
|
|
210
|
-
`VitestTestContext` is the running Vitest `TestContext` under the key `vitestTestContextKey`, provided on every test the fork runs. A library that carries its own view of the task context builds it on that key, so a case lane and a property lane read the same context:
|
|
258
|
+
`VitestTestContext` is the running Vitest `TestContext` under the key `vitestTestContextKey`, provided on every test the fork runs — every generator lane, both property lanes, and each `it.each` row. A library that carries its own view of the task context builds it on that key, so a case lane and a property lane read the same context:
|
|
211
259
|
|
|
212
260
|
```ts
|
|
213
261
|
import { vitestTestContextKey } from '@systemfsoftware/vitest'
|
|
@@ -218,16 +266,20 @@ export const TaskRef = Context.Reference<{ annotate?: (message: string) => void
|
|
|
218
266
|
})
|
|
219
267
|
```
|
|
220
268
|
|
|
269
|
+
There is no `owned`, no `recordAssertion` and no `@systemfsoftware/vitest/utils`: a library reports through the caller's check, and the one-check rule is what keeps a verdict a verdict.
|
|
270
|
+
|
|
221
271
|
## Compatibility with @effect/vitest
|
|
222
272
|
|
|
223
|
-
|
|
273
|
+
This is a fork of the surface, not a superset of it. What changed:
|
|
224
274
|
|
|
225
|
-
- `
|
|
226
|
-
- `
|
|
227
|
-
-
|
|
228
|
-
-
|
|
275
|
+
- **No `expect` and no `assert`**, from this package or from its `vitest` re-export; the callback parameter replaces both (and the guard refuses a raw one).
|
|
276
|
+
- **No `it.effect`, `it.scoped`, `it.scopedLive`, `owned`, `recordAssertion`, `@systemfsoftware/vitest/utils`, or `@systemfsoftware/vitest/refusals`.** `it.effect.prop` remains. `it.live` is a generator body on the real clock.
|
|
277
|
+
- **`export * from "vitest"` is gone.** The Vitest values the fork keeps — `vi`, `expectTypeOf`, `assertType`, `beforeAll`, `afterAll`, `onTestFailed`, `onTestFinished`, `inject`, `recordArtifact`, `vitest` — are re-exported explicitly, so a refused value cannot arrive through a star export.
|
|
278
|
+
- **A test body is a generator**, so a sync, `async` or Effect-returning body is refused with the rewrite.
|
|
279
|
+
- **`describe` is the fork's lawful collector** (forced concurrency and shuffle, the fork's methods), not upstream's. Vitest's own collector stays reachable as `import { describe } from 'vitest'`.
|
|
280
|
+
- **`skipIf` and `runIf` take a `boolean`**, where upstream took `unknown`; an `unknown` or `any` condition is refused by this repo's lint.
|
|
229
281
|
|
|
230
|
-
What else differs is what runs by default, what
|
|
282
|
+
What else differs is what runs by default, what a check refuses, and what `toEqual` means — that is why the fork is imported under its own name rather than aliasing upstream.
|
|
231
283
|
|
|
232
284
|
This package is part of [systemfsoftware](https://github.com/systemfsoftware/systemfsoftware).
|
|
233
285
|
|
package/dist/TestClock.d.ts
CHANGED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
|
|
2
|
+
import * as V from "vitest";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Context from "effect/Context";
|
|
5
|
+
//#region src/internal/refusals.d.ts
|
|
6
|
+
/** @internal */
|
|
7
|
+
declare const refusedText: {
|
|
8
|
+
readonly toBeDefined: '✗ toBeDefined passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).';
|
|
9
|
+
readonly toBeTruthy: '✗ toBeTruthy passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).';
|
|
10
|
+
readonly toBeFalsy: '✗ toBeFalsy passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).';
|
|
11
|
+
readonly toHaveLength: '✗ toHaveLength checks how many, not which. Assert the contents: toEqual([...]); only some of them: toEqual(expect.arrayContaining([...])).';
|
|
12
|
+
readonly toHaveProperty: '✗ toHaveProperty checks one key at a time. Assert the shape once: toMatchObject({ key: value, ... }).';
|
|
13
|
+
readonly toBeInstanceOf: '✗ toBeInstanceOf checks the kind of value, not the value. Assert the value: toEqual(expected); only its shape: toEqual(expect.schemaMatching(Schema)).';
|
|
14
|
+
readonly toBeTypeOf: '✗ toBeTypeOf checks the kind of value, not the value. Assert the value: toEqual(expected); only its shape: toEqual(expect.schemaMatching(Schema)).';
|
|
15
|
+
readonly toHaveBeenCalled: '✗ toHaveBeenCalled passes for any call with any arguments. Assert what the call carried: toHaveBeenCalledExactlyOnceWith(...args).';
|
|
16
|
+
readonly toHaveBeenCalledTimes: '✗ toHaveBeenCalledTimes counts calls, not what they carried. Assert the calls: toHaveBeenNthCalledWith(n, ...args), or all of them: expect(spy.mock.calls).toEqual([[...], [...]]).';
|
|
17
|
+
readonly toMatchSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. State the expected value: toEqual(expected).';
|
|
18
|
+
readonly toMatchInlineSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. State the expected value: toEqual(expected).';
|
|
19
|
+
readonly toMatchFileSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. State the expected value: toEqual(expected).';
|
|
20
|
+
readonly toThrowErrorMatchingSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. Name the error: toThrow(MyError).';
|
|
21
|
+
readonly toThrowErrorMatchingInlineSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. Name the error: toThrow(MyError).';
|
|
22
|
+
readonly resolves: '✗ .resolves/.rejects assert on a Promise outside the test runtime. Yield the Effect: const value = yield* program; its failure: const error = yield* Effect.flip(program); then yield* expect(error).toEqual(new MyError(...)).';
|
|
23
|
+
readonly rejects: '✗ .resolves/.rejects assert on a Promise outside the test runtime. Yield the Effect: const value = yield* program; its failure: const error = yield* Effect.flip(program); then yield* expect(error).toEqual(new MyError(...)).';
|
|
24
|
+
readonly anything: '✗ expect.anything() matches every value. Say what the value must be: expect.any(Money), expect.objectContaining({ ... }), expect.schemaMatching(Schema).';
|
|
25
|
+
readonly poll: '✗ expect.poll waits on the real clock, outside the runner\'s virtual time. Let time pass inside the test: yield* TestClock.adjust("3 seconds"); then yield* expect(value).toEqual(expected).';
|
|
26
|
+
readonly soft: '✗ expect.soft reports several failures on one state; the fork reports the state once. Assert the state once: yield* expect(actual).toEqual(expected).';
|
|
27
|
+
};
|
|
28
|
+
/** @internal */
|
|
29
|
+
type refusedText = typeof refusedText;
|
|
30
|
+
/** @internal */
|
|
31
|
+
declare const negatedText: {
|
|
32
|
+
readonly toBeDefined: '✗ toBeDefined passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).';
|
|
33
|
+
readonly toBeTruthy: '✗ toBeTruthy passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).';
|
|
34
|
+
readonly toBeFalsy: '✗ toBeFalsy passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).';
|
|
35
|
+
readonly toHaveLength: '✗ toHaveLength checks how many, not which. Assert the contents: toEqual([...]); only some of them: toEqual(expect.arrayContaining([...])).';
|
|
36
|
+
readonly toHaveProperty: '✗ toHaveProperty checks one key at a time. Assert the shape once: toMatchObject({ key: value, ... }).';
|
|
37
|
+
readonly toBeInstanceOf: '✗ toBeInstanceOf checks the kind of value, not the value. Assert the value: toEqual(expected); only its shape: toEqual(expect.schemaMatching(Schema)).';
|
|
38
|
+
readonly toBeTypeOf: '✗ toBeTypeOf checks the kind of value, not the value. Assert the value: toEqual(expected); only its shape: toEqual(expect.schemaMatching(Schema)).';
|
|
39
|
+
readonly toHaveBeenCalled: '✗ toHaveBeenCalled passes for any call with any arguments. Assert what the call carried: toHaveBeenCalledExactlyOnceWith(...args).';
|
|
40
|
+
readonly toHaveBeenCalledTimes: '✗ toHaveBeenCalledTimes counts calls, not what they carried. Assert the calls: toHaveBeenNthCalledWith(n, ...args), or all of them: expect(spy.mock.calls).toEqual([[...], [...]]).';
|
|
41
|
+
readonly toMatchSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. State the expected value: toEqual(expected).';
|
|
42
|
+
readonly toMatchInlineSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. State the expected value: toEqual(expected).';
|
|
43
|
+
readonly toMatchFileSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. State the expected value: toEqual(expected).';
|
|
44
|
+
readonly toThrowErrorMatchingSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. Name the error: toThrow(MyError).';
|
|
45
|
+
readonly toThrowErrorMatchingInlineSnapshot: '✗ a snapshot records what the code does now, not what it should do, and cannot fail on the run that writes it. Name the error: toThrow(MyError).';
|
|
46
|
+
readonly resolves: '✗ .resolves/.rejects assert on a Promise outside the test runtime. Yield the Effect: const value = yield* program; its failure: const error = yield* Effect.flip(program); then yield* expect(error).toEqual(new MyError(...)).';
|
|
47
|
+
readonly rejects: '✗ .resolves/.rejects assert on a Promise outside the test runtime. Yield the Effect: const value = yield* program; its failure: const error = yield* Effect.flip(program); then yield* expect(error).toEqual(new MyError(...)).';
|
|
48
|
+
readonly anything: '✗ expect.anything() matches every value. Say what the value must be: expect.any(Money), expect.objectContaining({ ... }), expect.schemaMatching(Schema).';
|
|
49
|
+
readonly poll: '✗ expect.poll waits on the real clock, outside the runner\'s virtual time. Let time pass inside the test: yield* TestClock.adjust("3 seconds"); then yield* expect(value).toEqual(expected).';
|
|
50
|
+
readonly soft: '✗ expect.soft reports several failures on one state; the fork reports the state once. Assert the state once: yield* expect(actual).toEqual(expected).';
|
|
51
|
+
readonly toBeNull: '✗ not.toBeNull passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).';
|
|
52
|
+
readonly toBeUndefined: '✗ not.toBeUndefined passes for almost any value the code returns. Assert the value: yield* expect(actual).toEqual(expected).';
|
|
53
|
+
};
|
|
54
|
+
/** @internal */
|
|
55
|
+
type negatedText = typeof negatedText;
|
|
56
|
+
/** @internal */
|
|
57
|
+
declare const neededText: {
|
|
58
|
+
readonly toThrow: '✗ toThrow() passes for any error. Name the one expected: toThrow(MyError) or toThrow("message"). An Effect failure is a value: const error = yield* Effect.flip(program); yield* expect(error).toEqual(new MyError(...)).';
|
|
59
|
+
readonly toThrowError: '✗ toThrowError() passes for any error. Name the one expected: toThrow(MyError) or toThrow("message"). An Effect failure is a value: const error = yield* Effect.flip(program); yield* expect(error).toEqual(new MyError(...)).';
|
|
60
|
+
readonly toSatisfy: '✗ toSatisfy(predicate) fails with \'expected x to satisfy [Function]\'. Say what must hold: toSatisfy(predicate, "every line has a positive quantity").';
|
|
61
|
+
readonly toMatchObject: '✗ toMatchObject({}) matches every object. Name the fields that must hold: toMatchObject({ status: "Pending" }).';
|
|
62
|
+
};
|
|
63
|
+
/** @internal */
|
|
64
|
+
type neededText = typeof neededText;
|
|
65
|
+
/** @internal */
|
|
66
|
+
declare const booleanRefusal = "✗ expect(<boolean>) can only report 'expected false to be true'. Pass the two values instead: yield* expect(a).toEqual(b), which uses Effect Equal. A predicate: yield* expect(value).toSatisfy(predicate, \"what must hold\").";
|
|
67
|
+
/** @internal */
|
|
68
|
+
type booleanRefusal = typeof booleanRefusal;
|
|
69
|
+
/** @internal */
|
|
70
|
+
declare const noCheckRefusal = "✗ this test yields no check, so it cannot fail. Yield one from the test's own expect: it(name, function* ({ expect }) { yield* expect(actual).toEqual(expected) }). An expect imported from vitest does not count.";
|
|
71
|
+
/** @internal */
|
|
72
|
+
type noCheckRefusal = typeof noCheckRefusal;
|
|
73
|
+
/** @internal */
|
|
74
|
+
type NoCheckRefusal = typeof noCheckRefusal;
|
|
75
|
+
/** @internal */
|
|
76
|
+
declare const unprovidedText = "✗ the test needs a service that nothing provides. Write the test inside layer(Service.layer)((it) => { it(name, ...) }); every test gets its own fresh build.";
|
|
77
|
+
/** @internal */
|
|
78
|
+
type unprovidedText = typeof unprovidedText;
|
|
79
|
+
/** @internal */
|
|
80
|
+
type UnprovidedRefusal = typeof unprovidedText;
|
|
81
|
+
/** @internal */
|
|
82
|
+
declare const refuseEffectBody = "✗ the body returned an Effect, so the runner cannot see its steps. Pass the generator itself: it(name, function* ({ expect }) { ... }).";
|
|
83
|
+
/** @internal */
|
|
84
|
+
type refuseEffectBody = typeof refuseEffectBody;
|
|
85
|
+
/** @internal */
|
|
86
|
+
type RefuseEffectBody = typeof refuseEffectBody;
|
|
87
|
+
/** @internal */
|
|
88
|
+
declare const refuseAsyncBody = "✗ an async body runs outside the test runtime. Pass a generator: it(name, function* ({ expect }) { yield* expect(actual).toEqual(expected) }).";
|
|
89
|
+
/** @internal */
|
|
90
|
+
type refuseAsyncBody = typeof refuseAsyncBody;
|
|
91
|
+
/** @internal */
|
|
92
|
+
type RefuseAsyncBody = typeof refuseAsyncBody;
|
|
93
|
+
/** @internal */
|
|
94
|
+
declare const refuseSyncBody = "✗ the body must be a generator that yields its checks: it(name, function* ({ expect }) { const x = yield* program; yield* expect(x).toEqual(expected) }).";
|
|
95
|
+
/** @internal */
|
|
96
|
+
type refuseSyncBody = typeof refuseSyncBody;
|
|
97
|
+
/** @internal */
|
|
98
|
+
type RefuseSyncBody = typeof refuseSyncBody;
|
|
99
|
+
/** @internal */
|
|
100
|
+
declare const refuseEffectLane = "✗ it.effect is removed: the body is the generator itself, so the runner sees every step. it(name, function* ({ expect }) { const x = yield* program; yield* expect(x).toEqual(expected) }).";
|
|
101
|
+
/** @internal */
|
|
102
|
+
type refuseEffectLane = typeof refuseEffectLane;
|
|
103
|
+
/** @internal */
|
|
104
|
+
type EffectLaneRefusal = typeof refuseEffectLane;
|
|
105
|
+
/** @internal */
|
|
106
|
+
declare const refuseScopedLane = "✗ it.scoped is removed: the body is the generator itself, so the runner sees every step. it(name, function* ({ expect }) { const x = yield* program; yield* expect(x).toEqual(expected) }).";
|
|
107
|
+
/** @internal */
|
|
108
|
+
type refuseScopedLane = typeof refuseScopedLane;
|
|
109
|
+
/** @internal */
|
|
110
|
+
type ScopedLaneRefusal = typeof refuseScopedLane;
|
|
111
|
+
/** @internal */
|
|
112
|
+
declare const refuseScopedLiveLane = "✗ it.scopedLive is removed: the body is the generator itself, so the runner sees every step. it(name, function* ({ expect }) { const x = yield* program; yield* expect(x).toEqual(expected) }).";
|
|
113
|
+
/** @internal */
|
|
114
|
+
type refuseScopedLiveLane = typeof refuseScopedLiveLane;
|
|
115
|
+
/** @internal */
|
|
116
|
+
type ScopedLiveLaneRefusal = typeof refuseScopedLiveLane;
|
|
117
|
+
/** @internal */
|
|
118
|
+
declare const refuseHook = "✗ hooks share state between tests. Build what a test needs inside it; services come fresh per test from layer(Service.layer)((it) => { ... }).";
|
|
119
|
+
/** @internal */
|
|
120
|
+
type refuseHook = typeof refuseHook;
|
|
121
|
+
/** @internal */
|
|
122
|
+
type HookRefusal = typeof refuseHook;
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/internal/checks.d.ts
|
|
125
|
+
/** An opaque runtime value: the fork does not read it, only hands it back. */
|
|
126
|
+
type Opaque<A = unknown> = A;
|
|
127
|
+
type Assertion<A> = V.Assertion<void, A>;
|
|
128
|
+
/** @internal */
|
|
129
|
+
type MakeAssertion = <A>(actual: A, message?: string) => Assertion<A>;
|
|
130
|
+
/** @internal */
|
|
131
|
+
type Judgement = (make: MakeAssertion) => void;
|
|
132
|
+
/**
|
|
133
|
+
* The service a check needs. `judge` counts the check and throws {@link refusePiecewise} when a check already ran
|
|
134
|
+
* since the last {@link AssertedShape.step}; `step` opens a new observed state.
|
|
135
|
+
*
|
|
136
|
+
* @internal
|
|
137
|
+
*/
|
|
138
|
+
interface AssertedShape {
|
|
139
|
+
readonly judge: (judgement: Judgement) => void;
|
|
140
|
+
readonly step: () => void;
|
|
141
|
+
}
|
|
142
|
+
declare const Asserted_base: Context.ServiceClass<Asserted, "@systemfsoftware/vitest/Asserted", AssertedShape>;
|
|
143
|
+
/** @internal */
|
|
144
|
+
declare class Asserted extends Asserted_base {}
|
|
145
|
+
/**
|
|
146
|
+
* An Effect that performs one check. Only the runner can provide what it needs.
|
|
147
|
+
*
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
type Check = Effect.Effect<void, never, Asserted>;
|
|
151
|
+
type Refusal<T extends string> = (this: T, ...args: ReadonlyArray<Opaque>) => Check;
|
|
152
|
+
type StaticRefusal<T extends string> = (this: T, ...args: ReadonlyArray<Opaque>) => never;
|
|
153
|
+
/** Callable only with the arguments that make it specific; the empty call's `this` type is the rewrite. */
|
|
154
|
+
interface Needs<Args extends ReadonlyArray<Opaque>, T extends string> {
|
|
155
|
+
(...args: Args): Check;
|
|
156
|
+
(this: T, ...args: ReadonlyArray<Opaque>): Check;
|
|
157
|
+
}
|
|
158
|
+
type MatcherName = Extract<keyof Assertion<Opaque>, `to${string}`>;
|
|
159
|
+
type ArgsOf<A, K extends MatcherName> = Assertion<A>[K] extends ((...args: infer P) => infer _) ? P : never;
|
|
160
|
+
type Refused = Extract<keyof typeof refusedText, MatcherName>;
|
|
161
|
+
type Needed = keyof typeof neededText;
|
|
162
|
+
type NegatedRefused = Extract<keyof typeof negatedText, MatcherName>;
|
|
163
|
+
/** The values `toThrow(X)` names the error with: a message, a pattern, an error class, or an error instance. */
|
|
164
|
+
type ThrowExpected = string | RegExp | (abstract new (...args: ReadonlyArray<never>) => Opaque) | Error;
|
|
165
|
+
/** @internal */
|
|
166
|
+
type Positive<A> = { readonly [K in Exclude<MatcherName, Refused | Needed>]: (...args: ArgsOf<A, K>) => Check; } & { readonly [K in Refused]: Refusal<(typeof refusedText)[K]>; } & {
|
|
167
|
+
readonly toThrow: Needs<[expected: ThrowExpected], (typeof neededText)['toThrow']>;
|
|
168
|
+
readonly toThrowError: Needs<[expected: ThrowExpected], (typeof neededText)['toThrowError']>;
|
|
169
|
+
readonly toSatisfy: Needs<[predicate: (value: A) => boolean, holds: string], (typeof neededText)['toSatisfy']>;
|
|
170
|
+
readonly toMatchObject: Needs<ArgsOf<A, 'toMatchObject'>, (typeof neededText)['toMatchObject']>;
|
|
171
|
+
readonly resolves: (typeof refusedText)['resolves'];
|
|
172
|
+
readonly rejects: (typeof refusedText)['rejects'];
|
|
173
|
+
};
|
|
174
|
+
/** @internal */
|
|
175
|
+
type Negative<A> = { readonly [K in Exclude<MatcherName, NegatedRefused>]: (...args: ArgsOf<A, K>) => Check; } & { readonly [K in NegatedRefused]: Refusal<(typeof negatedText)[K]>; };
|
|
176
|
+
/** @internal */
|
|
177
|
+
type Checked<A> = Positive<A> & {
|
|
178
|
+
readonly not: Negative<A>;
|
|
179
|
+
};
|
|
180
|
+
/** An asymmetric matcher: an opaque value vitest reads when the check runs. */
|
|
181
|
+
type Matcher = object;
|
|
182
|
+
/**
|
|
183
|
+
* The asymmetric matchers vitest builds: each names what a value must be, and only vitest reads the result.
|
|
184
|
+
*
|
|
185
|
+
* @internal
|
|
186
|
+
*/
|
|
187
|
+
interface Matchers {
|
|
188
|
+
readonly objectContaining: <T = Opaque>(expected: V.DeeplyAllowMatchers<T>) => Matcher;
|
|
189
|
+
readonly arrayContaining: <T = Opaque>(expected: Array<V.DeeplyAllowMatchers<T>>) => Matcher;
|
|
190
|
+
readonly stringContaining: (expected: string) => Matcher;
|
|
191
|
+
readonly stringMatching: (expected: string | RegExp) => Matcher;
|
|
192
|
+
readonly closeTo: (expected: number, precision?: number) => Matcher;
|
|
193
|
+
/** Accepts an Effect Schema directly, or any Standard Schema. */
|
|
194
|
+
readonly schemaMatching: (schema: Opaque) => Matcher;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* `expect.not.<matcher>`: the four asymmetric matchers whose negation states an absence honestly.
|
|
198
|
+
*
|
|
199
|
+
* @internal
|
|
200
|
+
*/
|
|
201
|
+
type NegatedMatchers = Pick<Matchers, 'objectContaining' | 'arrayContaining' | 'stringContaining' | 'stringMatching'>;
|
|
202
|
+
/**
|
|
203
|
+
* The asymmetric matchers that name what a value must be, the same matchers negated, and the statics the fork
|
|
204
|
+
* refuses by name.
|
|
205
|
+
*
|
|
206
|
+
* @internal
|
|
207
|
+
*/
|
|
208
|
+
interface Shapes extends Matchers {
|
|
209
|
+
readonly any: (constructor: Opaque) => Matcher;
|
|
210
|
+
/**
|
|
211
|
+
* `expect.not.<matcher>`, forwarded to vitest's own negated statics (`@vitest/expect/dist/index.d.ts:172`
|
|
212
|
+
* declares `not: AsymmetricMatchersContaining`); only the four honest absences are exposed.
|
|
213
|
+
*/
|
|
214
|
+
readonly not: NegatedMatchers;
|
|
215
|
+
readonly anything: StaticRefusal<(typeof refusedText)['anything']>;
|
|
216
|
+
readonly poll: StaticRefusal<(typeof refusedText)['poll']>;
|
|
217
|
+
readonly soft: StaticRefusal<(typeof refusedText)['soft']>;
|
|
218
|
+
}
|
|
219
|
+
/** @internal */
|
|
220
|
+
interface Expect extends Shapes {
|
|
221
|
+
<A>(actual: [A] extends [boolean] ? (typeof booleanRefusal) : A, message?: string): Checked<A>;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* What a test body receives.
|
|
225
|
+
*
|
|
226
|
+
* @internal
|
|
227
|
+
*/
|
|
228
|
+
interface Checks {
|
|
229
|
+
readonly expect: Expect;
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
export { Expect as a, NoCheckRefusal as c, RefuseSyncBody as d, ScopedLaneRefusal as f, Checks as i, RefuseAsyncBody as l, UnprovidedRefusal as m, AssertedShape as n, EffectLaneRefusal as o, ScopedLiveLaneRefusal as p, Check as r, HookRefusal as s, Asserted as t, RefuseEffectBody as u };
|