diff --git a/expect/_asymmetric_matchers.ts b/expect/_asymmetric_matchers.ts index c41b477d4..70bf85ebd 100644 --- a/expect/_asymmetric_matchers.ts +++ b/expect/_asymmetric_matchers.ts @@ -1,6 +1,8 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // deno-lint-ignore-file no-explicit-any +import { equal } from "./_equal.ts"; + export abstract class AsymmetricMatcher { constructor( protected value: T, @@ -145,3 +147,31 @@ export class StringMatching extends AsymmetricMatcher { export function stringMatching(pattern: string | RegExp): StringMatching { return new StringMatching(pattern); } + +export class ObjectContaining + extends AsymmetricMatcher> { + constructor(obj: Record) { + super(obj); + } + + equals(other: Record): boolean { + const keys = Object.keys(this.value); + + for (const key of keys) { + if ( + !Object.hasOwn(other, key) || + !equal(this.value[key], other[key]) + ) { + return false; + } + } + + return true; + } +} + +export function objectContaining( + obj: Record, +): ObjectContaining { + return new ObjectContaining(obj); +} diff --git a/expect/_object_containing_test.ts b/expect/_object_containing_test.ts new file mode 100644 index 000000000..b59276b60 --- /dev/null +++ b/expect/_object_containing_test.ts @@ -0,0 +1,50 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { Buffer } from "node:buffer"; +import { Buffer as DenoBuffer } from "@std/io/buffer"; +import { expect } from "./expect.ts"; + +Deno.test("expect.objectContaining()", () => { + expect({ bar: "baz" }).toEqual(expect.objectContaining({ bar: "baz" })); + expect({ foo: undefined }).toEqual( + expect.objectContaining({ foo: undefined }), + ); + expect({ bar: "baz" }).not.toEqual(expect.objectContaining({ foo: "bar" })); +}); + +Deno.test("expect.objectContaining() with nested objects", () => { + expect({ foo: { bar: "baz" } }).toEqual( + expect.objectContaining({ foo: { bar: "baz" } }), + ); + expect({ foo: { bar: "baz" } }).not.toEqual( + expect.objectContaining({ foo: { bar: "bar" } }), + ); +}); + +Deno.test("expect.objectContaining() with symbols", () => { + const foo = Symbol("foo"); + expect({ [foo]: { bar: "baz" } }).toEqual( + expect.objectContaining({ [foo]: { bar: "baz" } }), + ); +}); + +Deno.test("expect.objectContaining() with nested arrays", () => { + expect({ foo: ["bar", "baz"] }).toEqual( + expect.objectContaining({ foo: ["bar", "baz"] }), + ); + expect({ foo: ["bar", "baz"] }).not.toEqual( + expect.objectContaining({ foo: ["bar", "bar"] }), + ); +}); + +Deno.test("expect.objectContaining() with Node Buffer", () => { + expect({ foo: Buffer.from("foo") }).toEqual( + expect.objectContaining({ foo: Buffer.from("foo") }), + ); +}); + +Deno.test("expect.objectContaining() with Deno Buffer", () => { + expect({ foo: new DenoBuffer([1, 2, 3]) }).toEqual( + expect.objectContaining({ foo: new DenoBuffer([1, 2, 3]) }), + ); +}); diff --git a/expect/expect.ts b/expect/expect.ts index 49119d9be..30e763346 100644 --- a/expect/expect.ts +++ b/expect/expect.ts @@ -509,3 +509,21 @@ expect.stringMatching = asymmetricMatchers.stringMatching as ( * ``` */ expect.hasAssertions = hasAssertions as () => void; +/** + * `expect.objectContaining(object)` matches any received object that recursively matches the expected properties. + * That is, the expected object is not a subset of the received object. Therefore, it matches a received object + * which contains properties that are not in the expected object. + * + * @example + * ```ts + * import { expect } from "@std/expect"; + * + * Deno.test("example", () => { + * expect({ bar: 'baz' }).toEqual(expect.objectContaining({ bar: 'bar'})); + * expect({ bar: 'baz' }).not.toEqual(expect.objectContaining({ foo: 'bar'})); + * }); + * ``` + */ +expect.objectContaining = asymmetricMatchers.objectContaining as ( + obj: Record, +) => ReturnType; diff --git a/expect/mod.ts b/expect/mod.ts index da958b71e..65cda1274 100644 --- a/expect/mod.ts +++ b/expect/mod.ts @@ -54,6 +54,7 @@ * - {@linkcode expect.anything} * - {@linkcode expect.any} * - {@linkcode expect.arrayContaining} + * - {@linkcode expect.objectContaining} * - {@linkcode expect.closeTo} * - {@linkcode expect.stringContaining} * - {@linkcode expect.stringMatching} @@ -69,7 +70,6 @@ * - `toThrowErrorMatchingSnapshot` * - `toThrowErrorMatchingInlineSnapshot` * - Asymmetric matchers: - * - `expect.objectContaining` * - `expect.not.objectContaining` * - Utilities: * - `expect.assertions`