fix(expect): support expect.hasAssertions() (#5901)

Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
This commit is contained in:
eryue0220 2024-09-19 13:38:32 +08:00 committed by GitHub
parent ee9e3020e2
commit 6a4eb6cb91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 221 additions and 4 deletions

13
expect/_assertion.ts Normal file
View File

@ -0,0 +1,13 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { getAssertionState } from "@std/internal/assertion-state";
const assertionState = getAssertionState();
export function hasAssertions() {
assertionState.setAssertionCheck(true);
}
export function emitAssertionTrigger() {
assertionState.setAssertionTriggered(true);
}

33
expect/_assertion_test.ts Normal file
View File

@ -0,0 +1,33 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { describe, it, test } from "@std/testing/bdd";
import { expect } from "./expect.ts";
Deno.test("expect.hasAssertions() API", () => {
describe("describe suite", () => {
// FIXME(eryue0220): This test should throw `toThrowErrorMatchingSnapshot`
it("should throw an error", () => {
expect.hasAssertions();
});
it("should pass", () => {
expect.hasAssertions();
expect("a").toEqual("a");
});
});
it("it() suite should pass", () => {
expect.hasAssertions();
expect("a").toEqual("a");
});
// FIXME(eryue0220): This test should throw `toThrowErrorMatchingSnapshot`
test("test suite should throw an error", () => {
expect.hasAssertions();
});
test("test suite should pass", () => {
expect.hasAssertions();
expect("a").toEqual("a");
});
});

View File

@ -14,6 +14,7 @@ import type {
Matchers,
} from "./_types.ts";
import { AssertionError } from "@std/assert/assertion-error";
import { emitAssertionTrigger, hasAssertions } from "./_assertion.ts";
import {
addCustomEqualityTesters,
getCustomEqualityTesters,
@ -216,6 +217,8 @@ export function expect<T extends Expected = Expected>(
} else {
matcher(context, ...args);
}
emitAssertionTrigger();
}
return isPromised
@ -488,3 +491,21 @@ expect.stringContaining = asymmetricMatchers.stringContaining as (
expect.stringMatching = asymmetricMatchers.stringMatching as (
pattern: string | RegExp,
) => ReturnType<typeof asymmetricMatchers.stringMatching>;
/**
* `expect.hasAssertions` verifies that at least one assertion is called during a test.
*
* Note: expect.hasAssertions only can use in bdd function test suite, such as `test` or `it`.
*
* @example
* ```ts
*
* import { test } from "@std/testing/bdd";
* import { expect } from "@std/expect";
*
* test("it works", () => {
* expect.hasAssertions();
* expect("a").not.toBe("b");
* });
* ```
*/
expect.hasAssertions = hasAssertions as () => void;

View File

@ -60,19 +60,19 @@
* - Utilities:
* - {@linkcode expect.addEqualityTester}
* - {@linkcode expect.extend}
* - {@linkcode expect.hasAssertions}
*
* Only these functions are still not available:
* - Matchers:
* - `toMatchSnapShot`
* - `toMatchInlineSnapShot`
* - `toThrowErrorMatchingSnapShot`
* - `toThrowErrorMatchingInlineSnapShot`
* - `toMatchInlineSnapshot`
* - `toThrowErrorMatchingSnapshot`
* - `toThrowErrorMatchingInlineSnapshot`
* - Asymmetric matchers:
* - `expect.objectContaining`
* - `expect.not.objectContaining`
* - Utilities:
* - `expect.assertions`
* - `expect.hasAssertions`
* - `expect.addSnapshotSerializer`
*
* The tracking issue to add support for unsupported parts of the API is

111
internal/assertion_state.ts Normal file
View File

@ -0,0 +1,111 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
/**
* Check the test suite internal state
*
* @example Usage
* ```ts no-eval
* import { AssertionState } from "@std/internal";
*
* const assertionState = new AssertionState();
* ```
*/
export class AssertionState {
#state: {
assertionCheck: boolean;
assertionTriggered: boolean;
};
constructor() {
this.#state = {
assertionCheck: false,
assertionTriggered: false,
};
}
/**
* If `expect.hasAssertions` called, then through this method to update #state.assertionCheck value.
*
* @param val Set #state.assertionCheck's value
*
* @example Usage
* ```ts no-eval
* import { AssertionState } from "@std/internal";
*
* const assertionState = new AssertionState();
* assertionState.setAssertionCheck(true);
* ```
*/
setAssertionCheck(val: boolean) {
this.#state.assertionCheck = val;
}
/**
* If any matchers was called, `#state.assertionTriggered` will be set through this method.
*
* @param val Set #state.assertionTriggered's value
*
* @example Usage
* ```ts no-eval
* import { AssertionState } from "@std/internal";
*
* const assertionState = new AssertionState();
* assertionState.setAssertionTriggered(true);
* ```
*/
setAssertionTriggered(val: boolean) {
this.#state.assertionTriggered = val;
}
/**
* Check Assertion internal state, if `#state.assertionCheck` is set true, but
* `#state.assertionTriggered` is still false, then should throw an Assertion Error.
*
* @returns a boolean value, that the test suite is satisfied with the check. If not,
* it should throw an AssertionError.
*
* @example Usage
* ```ts no-eval
* import { AssertionState } from "@std/internal";
*
* const assertionState = new AssertionState();
* if (assertionState.checkAssertionErrorStateAndReset()) {
* // throw AssertionError("");
* }
* ```
*/
checkAssertionErrorStateAndReset(): boolean {
const result = this.#state.assertionCheck &&
!this.#state.assertionTriggered;
this.#resetAssertionState();
return result;
}
#resetAssertionState(): void {
this.#state = {
assertionCheck: false,
assertionTriggered: false,
};
}
}
const assertionState = new AssertionState();
/**
* return an instance of AssertionState
*
* @returns AssertionState
*
* @example Usage
* ```ts no-eval
* import { getAssertionState } from "@std/internal";
*
* const assertionState = getAssertionState();
* assertionState.setAssertionTriggered(true);
* ```
*/
export function getAssertionState(): AssertionState {
return assertionState;
}

View File

@ -0,0 +1,28 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals } from "@std/assert";
import { AssertionState } from "./assertion_state.ts";
Deno.test("AssertionState checkAssertionErrorStateAndReset pass", () => {
const assertionState = new AssertionState();
assertionState.setAssertionTriggered(true);
assertEquals(assertionState.checkAssertionErrorStateAndReset(), false);
});
Deno.test("AssertionState checkAssertionErrorStateAndReset pass", () => {
const assertionState = new AssertionState();
assertionState.setAssertionTriggered(true);
assertEquals(assertionState.checkAssertionErrorStateAndReset(), false);
assertionState.setAssertionCheck(true);
assertEquals(assertionState.checkAssertionErrorStateAndReset(), true);
});
Deno.test("AssertionState checkAssertionErrorStateAndReset fail", () => {
const assertionState = new AssertionState();
assertionState.setAssertionCheck(true);
assertEquals(assertionState.checkAssertionErrorStateAndReset(), true);
});

View File

@ -3,6 +3,7 @@
"version": "1.0.3",
"exports": {
".": "./mod.ts",
"./assertion-state": "./assertion_state.ts",
"./build-message": "./build_message.ts",
"./diff-str": "./diff_str.ts",
"./diff": "./diff.ts",

View File

@ -36,6 +36,7 @@
*
* @module
*/
export * from "./assertion_state.ts";
export * from "./build_message.ts";
export * from "./diff.ts";
export * from "./diff_str.ts";

View File

@ -402,6 +402,8 @@
* @module
*/
import { getAssertionState } from "@std/internal/assertion-state";
import { AssertionError } from "@std/assert/assertion-error";
import {
type DescribeDefinition,
type HookNames,
@ -565,6 +567,7 @@ export function it<T>(...args: ItArgs<T>) {
"Cannot register new test cases after already registered test cases start running",
);
}
const assertionState = getAssertionState();
const options = itDefinition(...args);
const { suite } = options;
const testSuite = suite
@ -594,6 +597,12 @@ export function it<T>(...args: ItArgs<T>) {
} finally {
TestSuiteInternal.runningCount--;
}
if (assertionState.checkAssertionErrorStateAndReset()) {
throw new AssertionError(
"Expected at least one assertion to be called but received none",
);
}
},
};
if (ignore !== undefined) {