1import { afterAll, assert, describe, describeWrapped, expect, it, layer } from "@effect/vitest"
2import * as testAssert from "@effect/vitest/utils"
3import { Clock, Context, Duration, Effect, Fiber, Layer, Schema } from "effect"
4import * as Arbitrary from "effect/Arbitrary"
5import { TestClock } from "effect/testing"
6
7it.effect(
8 "effect",
9 () => Effect.acquireRelease(Effect.sync(() => expect(1).toEqual(1)), () => Effect.void)
10)
11it.live(
12 "live",
13 () => Effect.acquireRelease(Effect.sync(() => expect(1).toEqual(1)), () => Effect.void)
14)
15
16it("throws fails when the thunk does not throw", () => {
17 expect(() => testAssert.throws(() => {})).toThrow()
18})
19
20it("throwsAsync fails when the promise resolves", async () => {
21 await expect(testAssert.throwsAsync(() => Promise.resolve())).rejects.toThrow()
22})
23
24it("deepStrictEqual fails with the assertion diff when no message is given", () => {
25 expect(() => testAssert.deepStrictEqual({ a: 1 }, { a: 2 })).toThrow("Expected values to be strictly deep-equal")
26})
27
28it("notDeepStrictEqual fails with the assertion message when no message is given", () => {
29 expect(() => testAssert.notDeepStrictEqual({ a: 1 }, { a: 1 })).toThrow("not to be strictly deep-equal")
30})
31
32// each
33
34it.effect.each([1, 2, 3])(
35 "effect each %s",
36 (n) => Effect.acquireRelease(Effect.sync(() => expect(n).toEqual(n)), () => Effect.void)
37)
38it.live.each([1, 2, 3])(
39 "live each %s",
40 (n) => Effect.acquireRelease(Effect.sync(() => expect(n).toEqual(n)), () => Effect.void)
41)
42
43// skip
44
45it.live.skip(
46 "live skipped",
47 () => Effect.die("skipped anyway")
48)
49it.effect.skip(
50 "effect skipped",
51 () => Effect.die("skipped anyway")
52)
53
54// skipIf
55
56it.effect.skipIf(true)("effect skipIf (true)", () => Effect.die("skipped anyway"))
57it.effect.skipIf(false)("effect skipIf (false)", () => Effect.sync(() => expect(1).toEqual(1)))
58
59// runIf
60
61it.effect.runIf(true)("effect runIf (true)", () => Effect.sync(() => expect(1).toEqual(1)))
62it.effect.runIf(false)("effect runIf (false)", () => Effect.die("not run anyway"))
63
64// chained helpers
65
66it.describe.each(["foo", "bar"] as const)("describe.each %s", (text) => {
67 it.effect("runs an Effect test", () =>
68 Effect.sync(() => {
69 assert.include(["foo", "bar"], text)
70 }))
71})
72
73it.skip.each([1])("skip.each %s", () => assert.fail("skipped anyway"))
74
75// The following test is expected to fail because it simulates a test timeout.
76// Be aware that eventual "failure" of the test is only logged out.
77it.live.fails("interrupts on timeout", (ctx) =>
78 Effect.gen(function*() {
79 let acquired = false
80
81 ctx.onTestFailed(() => {
82 if (acquired) {
83 // oxlint-disable-next-line no-console
84 console.error("'effect is interrupted on timeout' @effect/vitest test failed")
85 }
86 })
87
88 yield* Effect.acquireRelease(
89 Effect.sync(() => acquired = true),
90 () => Effect.sync(() => acquired = false)
91 )
92 yield* Effect.sleep(1000)
93 }), 1)
94
95class Foo extends Context.Service<Foo, "foo">()("Foo") {
96 static layer = Layer.succeed(Foo)("foo")
97}
98
99class Bar extends Context.Service<Bar, "bar">()("Bar") {
100 static layer = Layer.effect(Bar)(Effect.map(Foo, () => "bar" as const))
101}
102
103class Sleeper extends Context.Service<Sleeper, {
104 readonly sleep: (ms: number) => Effect.Effect<void>
105}>()("Sleeper") {
106 static readonly layer = Layer.effect(Sleeper)(
107 Effect.gen(function*() {
108 const clock = yield* Clock.Clock
109
110 return {
111 sleep: (ms: number) => clock.sleep(Duration.millis(ms))
112 }
113 })
114 )
115}
116
117describe("layer", () => {
118 layer(Foo.layer)((it) => {
119 it.effect("adds context", () =>
120 Effect.gen(function*() {
121 const foo = yield* Foo
122 expect(foo).toEqual("foo")
123 }))
124
125 it.layer(Bar.layer)("nested", (it) => {
126 it.effect("adds context", () =>
127 Effect.gen(function*() {
128 const foo = yield* Foo
129 const bar = yield* Bar
130 expect(foo).toEqual("foo")
131 expect(bar).toEqual("bar")
132 }))
133 })
134
135 it.layer(Bar.layer)((it) => {
136 it.effect("without name", () =>
137 Effect.gen(function*() {
138 const foo = yield* Foo
139 const bar = yield* Bar
140 expect(foo).toEqual("foo")
141 expect(bar).toEqual("bar")
142 }))
143 })
144
145 describe("release", () => {
146 let released = false
147 afterAll(() => {
148 expect(released).toEqual(true)
149 })
150
151 class Scoped extends Context.Service<Scoped, "scoped">()("Scoped") {
152 static layer = Layer.effect(Scoped)(
153 Effect.acquireRelease(
154 Effect.succeed("scoped" as const),
155 () => Effect.sync(() => released = true)
156 )
157 )
158 }
159
160 it.layer(Scoped.layer)((it) => {
161 it.effect("adds context", () =>
162 Effect.gen(function*() {
163 const foo = yield* Foo
164 const scoped = yield* Scoped
165 expect(foo).toEqual("foo")
166 expect(scoped).toEqual("scoped")
167 }))
168 })
169
170 it.effect.prop(
171 "adds context",
172 [realNumber],
173 ([num]) =>
174 Effect.gen(function*() {
175 const foo = yield* Foo
176 expect(foo).toEqual("foo")
177 return num === num
178 }),
179 { arbitrary: { runs: 200 } }
180 )
181
182 it.effect.prop(
183 "adds context with a Schema property",
184 [Schema.Int],
185 ([value]) =>
186 Effect.gen(function*() {
187 const foo = yield* Foo
188 assert.strictEqual(foo, "foo")
189 assert.isTrue(Number.isInteger(value))
190 }),
191 { arbitrary: { runs: 5, seed: "vitest-arbitrary-layer" } }
192 )
193 })
194 })
195
196 describe("anonymous layer next to a Vitest fixture", () => {
197 const withValue = it.extend("value", () => 1)
198
199 layer(Foo.layer)((it) => {
200 it.effect("provides its context", () => Effect.map(Foo, (foo) => expect(foo).toEqual("foo")))
201 })
202
203 withValue("runs the fixture test", ({ value }) => {
204 expect(value).toEqual(1)
205 })
206 })
207
208 layer(Sleeper.layer)("test services", (it) => {
209 it.effect("TestClock", () =>
210 Effect.gen(function*() {
211 const sleeper = yield* Sleeper
212 const fiber = yield* Effect.forkChild(sleeper.sleep(100_000))
213 yield* Effect.yieldNow
214 yield* TestClock.adjust(100_000)
215 yield* Fiber.join(fiber)
216 }))
217 })
218
219 layer(Foo.layer)("with a name", (it) => {
220 describe("with a nested describe", () => {
221 it.effect("adds context", () =>
222 Effect.gen(function*() {
223 const foo = yield* Foo
224 expect(foo).toEqual("foo")
225 }))
226 })
227 it.effect("adds context", () =>
228 Effect.gen(function*() {
229 const foo = yield* Foo
230 expect(foo).toEqual("foo")
231 }))
232 })
233
234 layer(Sleeper.layer, { excludeTestServices: true })("live services", (it) => {
235 it.effect("Clock", () =>
236 Effect.gen(function*() {
237 const sleeper = yield* Sleeper
238 yield* sleeper.sleep(1)
239 }))
240 })
241})
242
243describeWrapped("describeWrapped", (it) => {
244 it.layer(Foo.layer)("named layer", (it) => {
245 it.effect("registers in its own suite", ({ task }) =>
246 Effect.gen(function*() {
247 assert.strictEqual(task.suite?.name, "named layer")
248 assert.isTrue(task.suite?.tasks.includes(task))
249 assert.strictEqual(yield* Foo, "foo")
250 }))
251 })
252})
253
254// property testing
255
256const realNumber = Schema.Finite
257const textArbitrary = Arbitrary.schema(Schema.Literals(["a", "b"]))
258
259it.prop(
260 "schema with array",
261 [Schema.String, Schema.Int],
262 ([text, count]) => typeof text === "string" && Number.isInteger(count)
263)
264
265it.prop(
266 "schema with object",
267 { text: Schema.String, count: Schema.Int },
268 ({ text, count }) => typeof text === "string" && Number.isInteger(count)
269)
270
271let mixedTupleRuns = 0
272let mixedRecordRuns = 0
273afterAll(() => {
274 assert.strictEqual(mixedTupleRuns, 5)
275 assert.strictEqual(mixedRecordRuns, 5)
276})
277
278it.prop(
279 "Schema and Arbitrary with array",
280 [Schema.Int, textArbitrary],
281 ([count, text]) => {
282 mixedTupleRuns++
283 assert.isTrue(Number.isInteger(count))
284 assert.include(["a", "b"], text)
285 },
286 { arbitrary: { runs: 5, maxDiscards: 0, seed: "vitest-mixed-tuple" } }
287)
288
289it.effect.prop(
290 "Schema and Arbitrary with object",
291 { count: Schema.Int, text: textArbitrary },
292 ({ count, text }) =>
293 Effect.sync(() => {
294 mixedRecordRuns++
295 assert.isTrue(Number.isInteger(count))
296 assert.include(["a", "b"], text)
297 }),
298 { arbitrary: { runs: 5, maxDiscards: 0, seed: "vitest-mixed-record" } }
299)
300
301it.prop("symmetry", [realNumber, Schema.Int], ([a, b]) => a + b === b + a)
302
303it.prop(
304 "symmetry with object",
305 { a: realNumber, b: Schema.Int },
306 ({ a, b }) => a + b === b + a
307)
308
309it.live.prop(
310 "schema with object",
311 { value: Schema.Int },
312 ({ value }) => Effect.sync(() => assert.isTrue(Number.isInteger(value)))
313)
314
315let arbitraryEffectRuns = 0
316afterAll(() => assert.strictEqual(arbitraryEffectRuns, 5))
317
318it.effect.prop(
319 "schema with Arbitrary options",
320 [Schema.String, Schema.Int],
321 ([text, count]) =>
322 Effect.sync(() => {
323 arbitraryEffectRuns++
324 assert.strictEqual(typeof text, "string")
325 assert.isTrue(Number.isInteger(count))
326 }),
327 { arbitrary: { runs: 5, maxDiscards: 0, seed: "vitest-arbitrary" } }
328)
329
330it.effect.prop("symmetry", [realNumber, Schema.Int], ([a, b]) =>
331 Effect.gen(function*() {
332 yield* Effect.void
333 assert.isTrue(a + b === b + a)
334 }))
335
336it.effect.prop("symmetry with object", { a: realNumber, b: Schema.Int }, ({ a, b }) =>
337 Effect.gen(function*() {
338 yield* Effect.void
339 assert.strictEqual(a + b, b + a)
340 }))
341
342it.effect.prop(
343 "should detect the substring",
344 { a: Schema.String, b: Schema.String, c: Schema.String },
345 ({ a, b, c }) =>
346 Effect.gen(function*() {
347 yield* Effect.scope
348 assert.include(a + b + c, b)
349 })
350)
351
352describe("property failures", () => {
353 const Input = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000 }))
354 const pureDefectValues: Array<number> = []
355 const effectDefectValues: Array<number> = []
356 let interruptedRuns = 0
357 let timeoutPropertyStarted = false
358 let timeoutPropertyReleased = false
359
360 afterAll(() => {
361 assert.deepStrictEqual(pureDefectValues, [8, 1])
362 assert.deepStrictEqual(effectDefectValues, [8, 1])
363 assert.strictEqual(interruptedRuns, 1)
364 assert.isTrue(timeoutPropertyStarted)
365 assert.isTrue(timeoutPropertyReleased)
366 })
367
368 it.prop(
369 "shrinks synchronous defects",
370 [Input],
371 ([value]) => {
372 pureDefectValues.push(value)
373 throw new Error("property defect")
374 },
375 { fails: true, arbitrary: { runs: 1, seed: "assertion-shrink" } }
376 )
377
378 it.effect.prop(
379 "shrinks Effect defects",
380 [Input],
381 ([value]) =>
382 Effect.sync(() => {
383 effectDefectValues.push(value)
384 assert.strictEqual(value, 0)
385 }),
386 { fails: true, arbitrary: { runs: 1, seed: "assertion-shrink" } }
387 )
388
389 it.effect.prop(
390 "preserves interruption",
391 [Input],
392 () => {
393 interruptedRuns++
394 return Effect.interrupt
395 },
396 { fails: true, arbitrary: { runs: 1, seed: "assertion-shrink" } }
397 )
398
399 it.effect.prop(
400 "interrupts property checking on timeout",
401 [Schema.Literal("value")],
402 () =>
403 Effect.acquireUseRelease(
404 Effect.sync(() => {
405 timeoutPropertyStarted = true
406 }),
407 () => Effect.never,
408 () =>
409 Effect.sync(() => {
410 timeoutPropertyReleased = true
411 })
412 ),
413 { fails: true, timeout: 10, arbitrary: { runs: 1, maxDiscards: 0, seed: "property-timeout" } }
414 )
415})