Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(expect): support expect.not.{arrayContaining,objectContaning,stringContaining,stringMatching} #6138

Merged
merged 3 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions expect/_array_containing_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@ Deno.test("expect.arrayContaining() with array of mixed types", () => {
expect([1, 2, 3, "hello", "bye"]).toEqual(expect.arrayContaining(arr));
expect([4, "bye"]).not.toEqual(expect.arrayContaining(arr));
});

Deno.test("expect.not.arrayContaining with array of numbers", () => {
const arr = [7, 8, 9];
expect([1, 2, 3, 4]).toEqual(expect.not.arrayContaining(arr));
expect([1, 2, 3, 4]).not.toEqual(expect.arrayContaining(arr));
expect([4, 5, 6, 7]).toEqual(expect.not.arrayContaining(arr));
expect([4, 5, 6, 7]).not.toEqual(expect.arrayContaining(arr));
});

Deno.test("expect.arrayContaining() with array of mixed types", () => {
const arr = [5, "world"];
expect([1, 2, 3, "hello", "bye"]).toEqual(expect.not.arrayContaining(arr));
expect([4, "bye"]).not.toEqual(expect.arrayContaining(arr));
});
58 changes: 37 additions & 21 deletions expect/_asymmetric_matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { equal } from "./_equal.ts";
export abstract class AsymmetricMatcher<T> {
constructor(
protected value: T,
protected inverse: boolean = false,
) {}
abstract equals(other: unknown): boolean;
}
Expand Down Expand Up @@ -69,19 +70,25 @@ export function any(c: unknown): Any {
}

export class ArrayContaining extends AsymmetricMatcher<any[]> {
constructor(arr: any[]) {
super(arr);
constructor(arr: any[], inverse = false) {
super(arr, inverse);
}

equals(other: any[]): boolean {
return Array.isArray(other) && this.value.every((e) => other.includes(e));
const res = Array.isArray(other) &&
this.value.every((e) => other.includes(e));
return this.inverse ? !res : res;
}
}

export function arrayContaining(c: any[]): ArrayContaining {
return new ArrayContaining(c);
}

export function arrayNotContaining(c: any[]): ArrayContaining {
return new ArrayContaining(c, true);
}

export class CloseTo extends AsymmetricMatcher<number> {
readonly #precision: number;

Expand Down Expand Up @@ -113,60 +120,63 @@ export function closeTo(num: number, numDigits?: number): CloseTo {
}

export class StringContaining extends AsymmetricMatcher<string> {
constructor(str: string) {
super(str);
constructor(str: string, inverse = false) {
super(str, inverse);
}

equals(other: string): boolean {
if (typeof other !== "string") {
return false;
}

return other.includes(this.value);
const res = typeof other !== "string" ? false : other.includes(this.value);
return this.inverse ? !res : res;
}
}

export function stringContaining(str: string): StringContaining {
return new StringContaining(str);
}

export function stringNotContaining(str: string): StringContaining {
return new StringContaining(str, true);
}

export class StringMatching extends AsymmetricMatcher<RegExp> {
constructor(pattern: string | RegExp) {
super(new RegExp(pattern));
constructor(pattern: string | RegExp, inverse = false) {
super(new RegExp(pattern), inverse);
}

equals(other: string): boolean {
if (typeof other !== "string") {
return false;
}

return this.value.test(other);
const res = typeof other !== "string" ? false : this.value.test(other);
return this.inverse ? !res : res;
}
}

export function stringMatching(pattern: string | RegExp): StringMatching {
return new StringMatching(pattern);
}

export function stringNotMatching(pattern: string | RegExp): StringMatching {
return new StringMatching(pattern, true);
}

export class ObjectContaining
extends AsymmetricMatcher<Record<string, unknown>> {
constructor(obj: Record<string, unknown>) {
super(obj);
constructor(obj: Record<string, unknown>, inverse = false) {
super(obj, inverse);
}

equals(other: Record<string, unknown>): boolean {
const keys = Object.keys(this.value);
let res = true;

for (const key of keys) {
if (
!Object.hasOwn(other, key) ||
!equal(this.value[key], other[key])
) {
return false;
res = false;
}
}

return true;
return this.inverse ? !res : res;
}
}

Expand All @@ -175,3 +185,9 @@ export function objectContaining(
): ObjectContaining {
return new ObjectContaining(obj);
}

export function objectNotContaining(
obj: Record<string, unknown>,
): ObjectContaining {
return new ObjectContaining(obj, true);
}
14 changes: 14 additions & 0 deletions expect/_object_containing_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,17 @@ Deno.test("expect.objectContaining() with Deno Buffer", () => {
expect.objectContaining({ foo: new DenoBuffer([1, 2, 3]) }),
);
});

Deno.test("expect.not.objectContaining()", () => {
expect({ bar: "baz" }).toEqual(expect.not.objectContaining({ foo: "bar" }));
expect({ foo: ["bar", "baz"] }).toEqual(
expect.not.objectContaining({ foo: ["bar", "bar"] }),
);
});

Deno.test("expect.not.objectContaining() with symbols", () => {
const foo = Symbol("foo");
expect({ [foo]: { bar: "baz" } }).toEqual(
expect.objectContaining({ [foo]: { bar: "bazzzz" } }),
);
});
14 changes: 14 additions & 0 deletions expect/_string_containing_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,17 @@ Deno.test("expect.stringContaining() with other types", () => {
expect(["foo", "bar"]).not.toEqual(expect.stringContaining("foo"));
expect({ foo: "bar" }).not.toEqual(expect.stringContaining(`{ foo: "bar" }`));
});

Deno.test("expect.not.stringContaining() with strings", () => {
expect("https://deno.com/").toEqual(expect.not.stringContaining("node"));
expect("deno").toEqual(expect.not.stringContaining("Deno"));
expect("foobar").toEqual(expect.not.stringContaining("bazz"));
expect("How are you?").toEqual(expect.not.stringContaining("Hello world!"));
});

Deno.test("expect.not.stringContaining() with other types", () => {
expect(123).toEqual(expect.not.stringContaining("1"));
expect(true).toEqual(expect.not.stringContaining("true"));
expect(["foo", "bar"]).toEqual(expect.not.stringContaining("foo"));
expect({ foo: "bar" }).toEqual(expect.not.stringContaining(`{ foo: "bar" }`));
});
8 changes: 8 additions & 0 deletions expect/_string_matching_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ Deno.test("expect.stringMatching() with RegExp", () => {
expect("\e").not.toEqual(expect.stringMatching(/\s/));
expect("queue").not.toEqual(expect.stringMatching(/en/));
});

Deno.test("expect.not.stringMatching()", () => {
expect("Hello, World").toEqual(expect.not.stringMatching("hello"));
expect("foobar").toEqual(expect.not.stringMatching("bazz"));
expect("How are you?").toEqual(expect.not.stringMatching(/Hello world!/));
expect("queue").toEqual(expect.not.stringMatching(/en/));
expect("\e").toEqual(expect.not.stringMatching(/\s/));
});
47 changes: 47 additions & 0 deletions expect/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,50 @@ expect.hasAssertions = hasAssertions as () => void;
expect.objectContaining = asymmetricMatchers.objectContaining as (
obj: Record<string, unknown>,
) => ReturnType<typeof asymmetricMatchers.objectContaining>;
/**
* `expect.not.arrayContaining` matches a received array which does not contain
* all of the elements in the expected array. That is, the expected array is not
* a subset of the received array.
*
* `expect.not.objectContaining` matches any received object that does not recursively
* match 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.
*
* `expect.not.stringContaining` matches the received value if it is not a string
* or if it is a string that does not contain the exact expected string.
*
* `expect.not.stringMatching` matches the received value if it is not a string
* or if it is a string that does not match the expected string or regular expression.
*
* @example
* ```ts
* import { expect } from "@std/expect";
*
* Deno.test("expect.not.arrayContaining", () => {
* const expected = ["Samantha"];
* expect(["Alice", "Bob", "Eve"]).toEqual(expect.not.arrayContaining(expected));
* });
*
* Deno.test("expect.not.objectContaining", () => {
* const expected = { foo: "bar" };
* expect({ bar: "baz" }).toEqual(expect.not.objectContaining(expected));
* });
*
* Deno.test("expect.not.stringContaining", () => {
* const expected = "Hello world!";
* expect("How are you?").toEqual(expect.not.stringContaining(expected));
* });
*
* Deno.test("expect.not.stringMatching", () => {
* const expected = /Hello world!/;
* expect("How are you?").toEqual(expect.not.stringMatching(expected));
* });
* ```
*/
expect.not = {
arrayContaining: asymmetricMatchers.arrayNotContaining,
objectContaining: asymmetricMatchers.objectNotContaining,
stringContaining: asymmetricMatchers.stringNotContaining,
stringMatching: asymmetricMatchers.stringNotMatching,
};
6 changes: 4 additions & 2 deletions expect/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@
* - {@linkcode expect.anything}
* - {@linkcode expect.any}
* - {@linkcode expect.arrayContaining}
* - {@linkcode expect.not.arrayContaining}
* - {@linkcode expect.objectContaining}
* - {@linkcode expect.not.objectContaining}
* - {@linkcode expect.closeTo}
* - {@linkcode expect.stringContaining}
* - {@linkcode expect.not.stringContaining}
* - {@linkcode expect.stringMatching}
* - {@linkcode expect.not.stringMatching}
* - Utilities:
* - {@linkcode expect.addEqualityTester}
* - {@linkcode expect.extend}
Expand All @@ -69,8 +73,6 @@
* - `toMatchInlineSnapshot`
* - `toThrowErrorMatchingSnapshot`
* - `toThrowErrorMatchingInlineSnapshot`
* - Asymmetric matchers:
* - `expect.not.objectContaining`
Comment on lines -72 to -73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

* - Utilities:
* - `expect.assertions`
* - `expect.addSnapshotSerializer`
Expand Down