diff --git a/expect/_assertion.ts b/expect/_assertion.ts new file mode 100644 index 000000000..7540c27b0 --- /dev/null +++ b/expect/_assertion.ts @@ -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); +} diff --git a/expect/_assertion_test.ts b/expect/_assertion_test.ts new file mode 100644 index 000000000..8a3cfd966 --- /dev/null +++ b/expect/_assertion_test.ts @@ -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"); + }); +}); diff --git a/expect/expect.ts b/expect/expect.ts index c999b499f..ad62988b7 100644 --- a/expect/expect.ts +++ b/expect/expect.ts @@ -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( } 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; +/** + * `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; diff --git a/expect/mod.ts b/expect/mod.ts index 38ce46645..da958b71e 100644 --- a/expect/mod.ts +++ b/expect/mod.ts @@ -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 diff --git a/internal/assertion_state.ts b/internal/assertion_state.ts new file mode 100644 index 000000000..5229945b2 --- /dev/null +++ b/internal/assertion_state.ts @@ -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; +} diff --git a/internal/assertion_state_test.ts b/internal/assertion_state_test.ts new file mode 100644 index 000000000..4383f36c5 --- /dev/null +++ b/internal/assertion_state_test.ts @@ -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); +}); diff --git a/internal/deno.json b/internal/deno.json index 76b227d0e..86ad7f9e1 100644 --- a/internal/deno.json +++ b/internal/deno.json @@ -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", diff --git a/internal/mod.ts b/internal/mod.ts index 170670af4..615ea9b60 100644 --- a/internal/mod.ts +++ b/internal/mod.ts @@ -36,6 +36,7 @@ * * @module */ +export * from "./assertion_state.ts"; export * from "./build_message.ts"; export * from "./diff.ts"; export * from "./diff_str.ts"; diff --git a/testing/bdd.ts b/testing/bdd.ts index fc21fc2bc..3258d46f4 100644 --- a/testing/bdd.ts +++ b/testing/bdd.ts @@ -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(...args: ItArgs) { "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(...args: ItArgs) { } finally { TestSuiteInternal.runningCount--; } + + if (assertionState.checkAssertionErrorStateAndReset()) { + throw new AssertionError( + "Expected at least one assertion to be called but received none", + ); + } }, }; if (ignore !== undefined) {