diff --git a/.changeset/forty-lizards-tap.md b/.changeset/forty-lizards-tap.md new file mode 100644 index 0000000000..77d40f02b8 --- /dev/null +++ b/.changeset/forty-lizards-tap.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Call Equal.equals internally in order inputs were passed. diff --git a/.changeset/shiny-crabs-bake.md b/.changeset/shiny-crabs-bake.md new file mode 100644 index 0000000000..ae53c02401 --- /dev/null +++ b/.changeset/shiny-crabs-bake.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Fix usage of `toMatchObject`. diff --git a/packages/effect/src/Equal.ts b/packages/effect/src/Equal.ts index 5523b1936b..83cb57b7ca 100644 --- a/packages/effect/src/Equal.ts +++ b/packages/effect/src/Equal.ts @@ -44,7 +44,13 @@ function compareBoth(self: unknown, that: unknown): boolean { if (selfType === "object" || selfType === "function") { if (self !== null && that !== null) { if (isEqual(self) && isEqual(that)) { - return Hash.hash(self) === Hash.hash(that) && self[symbol](that) + if (Hash.hash(self) === Hash.hash(that) && self[symbol](that)) { + return true + } else { + return structuralRegionState.enabled && structuralRegionState.tester + ? structuralRegionState.tester(self, that) + : false + } } } if (structuralRegionState.enabled) { diff --git a/packages/effect/src/Hash.ts b/packages/effect/src/Hash.ts index f940ef936e..6dd8b3f10e 100644 --- a/packages/effect/src/Hash.ts +++ b/packages/effect/src/Hash.ts @@ -36,6 +36,10 @@ export interface Hash { * @category hashing */ export const hash: (self: A) => number = (self: A) => { + if (structuralRegionState.enabled === true) { + return 0 + } + switch (typeof self) { case "number": return number(self) @@ -72,9 +76,6 @@ export const hash: (self: A) => number = (self: A) => { * @category hashing */ export const random: (self: A) => number = (self) => { - if (structuralRegionState.enabled === true) { - return 0 - } if (!randomHashCache.has(self)) { randomHashCache.set(self, number(pcgr.integer(Number.MAX_SAFE_INTEGER))) } @@ -171,36 +172,23 @@ export const cached: { if (arguments.length === 1) { const self = arguments[0] as object return function(hash: number) { - // @ts-expect-error - const original = self[symbol].bind(self) - if (structuralRegionState.enabled === false) { - Object.defineProperty(self, symbol, { - value() { - if (structuralRegionState.enabled === true) { - return original() - } - return hash - }, - enumerable: false - }) - } + Object.defineProperty(self, symbol, { + value() { + return hash + }, + enumerable: false + }) return hash } as any } const self = arguments[0] as object const hash = arguments[1] as number - // @ts-expect-error - const original = self[symbol].bind(self) - if (structuralRegionState.enabled === false) { - Object.defineProperty(self, symbol, { - value() { - if (structuralRegionState.enabled === true) { - return original() - } - return hash - }, - enumerable: false - }) - } + Object.defineProperty(self, symbol, { + value() { + return hash + }, + enumerable: false + }) + return hash } diff --git a/packages/effect/src/internal/either.ts b/packages/effect/src/internal/either.ts index 9bde0e76db..6ef2f5f55f 100644 --- a/packages/effect/src/internal/either.ts +++ b/packages/effect/src/internal/either.ts @@ -34,7 +34,7 @@ const RightProto = Object.assign(Object.create(CommonProto), { _tag: "Right", _op: "Right", [Equal.symbol](this: Either.Right, that: unknown): boolean { - return isEither(that) && isRight(that) && Equal.equals(that.right, this.right) + return isEither(that) && isRight(that) && Equal.equals(this.right, that.right) }, [Hash.symbol](this: Either.Right) { return Hash.combine(Hash.hash(this._tag))(Hash.hash(this.right)) @@ -52,7 +52,7 @@ const LeftProto = Object.assign(Object.create(CommonProto), { _tag: "Left", _op: "Left", [Equal.symbol](this: Either.Left, that: unknown): boolean { - return isEither(that) && isLeft(that) && Equal.equals(that.left, this.left) + return isEither(that) && isLeft(that) && Equal.equals(this.left, that.left) }, [Hash.symbol](this: Either.Left) { return Hash.combine(Hash.hash(this._tag))(Hash.hash(this.left)) diff --git a/packages/effect/src/internal/option.ts b/packages/effect/src/internal/option.ts index 68618ef800..7d97adee2c 100644 --- a/packages/effect/src/internal/option.ts +++ b/packages/effect/src/internal/option.ts @@ -28,7 +28,7 @@ const SomeProto = Object.assign(Object.create(CommonProto), { _tag: "Some", _op: "Some", [Equal.symbol](this: Option.Some, that: unknown): boolean { - return isOption(that) && isSome(that) && Equal.equals(that.value, this.value) + return isOption(that) && isSome(that) && Equal.equals(this.value, that.value) }, [Hash.symbol](this: Option.Some) { return Hash.cached(this, Hash.combine(Hash.hash(this._tag))(Hash.hash(this.value))) diff --git a/packages/effect/test/Either.test.ts b/packages/effect/test/Either.test.ts index adb3f4d617..5727792536 100644 --- a/packages/effect/test/Either.test.ts +++ b/packages/effect/test/Either.test.ts @@ -326,16 +326,6 @@ describe("Either", () => { Util.deepStrictEqual(pipe(Either.left("a"), Either.orElse(() => Either.left("b"))), Either.left("b")) }) - it("vitest equality", () => { - expect(Either.right(1)).toStrictEqual(Either.right(1)) - expect(Either.left(1)).toStrictEqual(Either.left(1)) - - expect(Either.right(2)).not.toStrictEqual(Either.right(1)) - expect(Either.left(2)).not.toStrictEqual(Either.left(1)) - expect(Either.left(1)).not.toStrictEqual(Either.right(1)) - expect(Either.left(1)).not.toStrictEqual(Either.right(2)) - }) - describe("do notation", () => { it("Do", () => { expectRight(Either.Do, {}) diff --git a/packages/effect/test/Exit.test.ts b/packages/effect/test/Exit.test.ts index 8034358b6f..6f2384dc3c 100644 --- a/packages/effect/test/Exit.test.ts +++ b/packages/effect/test/Exit.test.ts @@ -1,4 +1,3 @@ -import * as Cause from "effect/Cause" import * as Exit from "effect/Exit" import { describe, expect, it } from "vitest" @@ -74,18 +73,4 @@ describe("Exit", () => { }`) }) }) - - it("vitest equality", () => { - expect(Exit.succeed(1)).toEqual(Exit.succeed(1)) - expect(Exit.fail("failure")).toEqual(Exit.fail("failure")) - expect(Exit.die("defect")).toEqual(Exit.die("defect")) - - expect(Exit.succeed(1)).not.toEqual(Exit.succeed(2)) - expect(Exit.fail("failure")).not.toEqual(Exit.fail("failure1")) - expect(Exit.die("failure")).not.toEqual(Exit.fail("failure1")) - expect(Exit.die("failure")).not.toEqual(Exit.fail("failure1")) - expect(Exit.failCause(Cause.sequential(Cause.fail("f1"), Cause.fail("f2")))).not.toEqual( - Exit.failCause(Cause.sequential(Cause.fail("f1"), Cause.fail("f3"))) - ) - }) }) diff --git a/packages/effect/test/Option.test.ts b/packages/effect/test/Option.test.ts index 949ef69448..b8cbfc9f0b 100644 --- a/packages/effect/test/Option.test.ts +++ b/packages/effect/test/Option.test.ts @@ -499,14 +499,6 @@ describe("Option", () => { expect(f(Option.some(1), Option.some(2))).toStrictEqual(Option.some(3)) }) - it("vitest equality", () => { - expect(Option.some(2)).toStrictEqual(Option.some(2)) - expect(Option.none()).toStrictEqual(Option.none()) - - expect(Option.some(2)).not.toStrictEqual(Option.some(1)) - expect(Option.none()).not.toStrictEqual(Option.some(1)) - }) - describe("do notation", () => { it("Do", () => { expectSome(Option.Do, {}) diff --git a/packages/schema/test/Schema/Class/Class.test.ts b/packages/schema/test/Schema/Class/Class.test.ts index 0876df05f6..22e154e58a 100644 --- a/packages/schema/test/Schema/Class/Class.test.ts +++ b/packages/schema/test/Schema/Class/Class.test.ts @@ -49,7 +49,7 @@ describe("Class", () => { const schema = S.suspend(() => string) class A extends S.Class("A")({ a: S.optional(schema) }) {} const string = S.String - await Util.expectDecodeUnknownSuccess(A, { a: "a" }) + await Util.expectDecodeUnknownSuccess(A, new A({ a: "a" })) }) it("should be a Schema", () => { diff --git a/packages/schema/test/Schema/Class/extend.test.ts b/packages/schema/test/Schema/Class/extend.test.ts index a3e3dd9dd4..22c259654a 100644 --- a/packages/schema/test/Schema/Class/extend.test.ts +++ b/packages/schema/test/Schema/Class/extend.test.ts @@ -80,7 +80,7 @@ describe("extend", () => { S.Struct(fields).pipe(S.filter(({ a, b }) => a === b ? undefined : "a should be equal to b")) ) {} Util.expectFields(A.fields, { ...baseFields, ...fields }) - await Util.expectDecodeUnknownSuccess(A, { base: "base", a: 1, b: 1 }) + await Util.expectDecodeUnknownSuccess(A, new A({ base: "base", a: 1, b: 1 })) await Util.expectDecodeUnknownFailure( A, { base: "base", a: 1, b: 2 }, diff --git a/packages/vitest/src/index.ts b/packages/vitest/src/index.ts index c1a4a5570b..562ba6f4e8 100644 --- a/packages/vitest/src/index.ts +++ b/packages/vitest/src/index.ts @@ -1,7 +1,7 @@ /** * @since 1.0.0 */ -import type { TesterContext } from "@vitest/expect" +import type { Tester, TesterContext } from "@vitest/expect" import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import * as Equal from "effect/Equal" @@ -24,9 +24,16 @@ export type API = TestAPI<{}> const TestEnv = TestEnvironment.TestContext.pipe( Layer.provide(Logger.remove(Logger.defaultLogger)) ) + /** @internal */ -function customTester(this: TesterContext, a: unknown, b: unknown) { - return Utils.structuralRegion(() => Equal.equals(a, b), (x, y) => this.equals(x, y)) +function customTester(this: TesterContext, a: unknown, b: unknown, customTesters: Array) { + if (!Equal.isEqual(a) || !Equal.isEqual(b)) { + return undefined + } + return Utils.structuralRegion( + () => Equal.equals(a, b), + (x, y) => this.equals(x, y, customTesters.filter((t) => t !== customTester)) + ) } /** diff --git a/packages/vitest/test/equality-tester.test.ts b/packages/vitest/test/equality-tester.test.ts new file mode 100644 index 0000000000..819e04d449 --- /dev/null +++ b/packages/vitest/test/equality-tester.test.ts @@ -0,0 +1,79 @@ +import * as Cause from "effect/Cause" +import * as Data from "effect/Data" +import * as Either from "effect/Either" +import * as Exit from "effect/Exit" +import * as Option from "effect/Option" +import { describe, expect, it } from "vitest" + +describe("toMatchObject", () => { + it("plain objects", () => { + expect({ a: 1, b: 2 }).toMatchObject({ a: 1 }) + }) + + it("Data.struct", () => { + const alice = Data.struct({ name: "Alice", age: 30 }) + + expect(alice).toMatchObject(Data.struct({ name: "Alice" })) + }) + + it("option", () => { + expect(Option.some({ a: 1, b: 2 })).toMatchObject(Option.some({ a: 1 })) + expect(Option.none()).toMatchObject(Option.none()) + expect({ x: Option.some({ a: 1, b: 2 }), y: Option.none() }).toMatchObject({ x: Option.some({ a: 1 }) }) + + expect(Option.none()).not.toMatchObject(Option.some({ a: 1 })) + expect(Option.some({ b: 1 })).not.toMatchObject(Option.some({ a: 1 })) + expect({ x: Option.some({ a: 1, b: 2 }), y: Option.none() }).not.toMatchObject({ x: Option.some({ b: 1 }) }) + expect({ x: Option.none(), y: Option.none() }).not.toMatchObject({ x: Option.some({}) }) + }) + + it("either", () => { + expect(Either.right({ a: 1, b: 2 })).toMatchObject(Either.right({ a: 1 })) + expect(Either.left({ a: 1, b: 2 })).toMatchObject(Either.left({ a: 1 })) + + expect(Either.right({ a: 1, b: 2 })).not.toMatchObject(Either.left({ a: 1 })) + expect(Either.left({ a: 1, b: 2 })).not.toMatchObject(Either.right({ a: 1 })) + }) + + it("either", () => { + expect(Either.right({ a: 1, b: 2 })).toMatchObject(Either.right({ a: 1 })) + expect(Either.left({ a: 1, b: 2 })).toMatchObject(Either.left({ a: 1 })) + + expect(Either.right({ a: 1, b: 2 })).not.toMatchObject(Either.left({ a: 1 })) + expect(Either.left({ a: 1, b: 2 })).not.toMatchObject(Either.right({ a: 1 })) + }) +}) + +describe.each(["toStrictEqual", "toEqual"] as const)("%s", (matcher) => { + it("either", () => { + expect(Either.right(1))[matcher](Either.right(1)) + expect(Either.left(1))[matcher](Either.left(1)) + + expect(Either.right(2)).not[matcher](Either.right(1)) + expect(Either.left(2)).not[matcher](Either.left(1)) + expect(Either.left(1)).not[matcher](Either.right(1)) + expect(Either.left(1)).not[matcher](Either.right(2)) + }) + + it("exit", () => { + expect(Exit.succeed(1))[matcher](Exit.succeed(1)) + expect(Exit.fail("failure"))[matcher](Exit.fail("failure")) + expect(Exit.die("defect"))[matcher](Exit.die("defect")) + + expect(Exit.succeed(1)).not[matcher](Exit.succeed(2)) + expect(Exit.fail("failure")).not[matcher](Exit.fail("failure1")) + expect(Exit.die("failure")).not[matcher](Exit.fail("failure1")) + expect(Exit.die("failure")).not[matcher](Exit.fail("failure1")) + expect(Exit.failCause(Cause.sequential(Cause.fail("f1"), Cause.fail("f2")))).not[matcher]( + Exit.failCause(Cause.sequential(Cause.fail("f1"), Cause.fail("f3"))) + ) + }) + + it("option", () => { + expect(Option.some(2))[matcher](Option.some(2)) + expect(Option.none())[matcher](Option.none()) + + expect(Option.some(2)).not[matcher](Option.some(1)) + expect(Option.none()).not[matcher](Option.some(1)) + }) +})