Skip to content

Commit

Permalink
fix(expect): support expect.hasAssertions() (#5901)
Browse files Browse the repository at this point in the history
Co-authored-by: Yoshiya Hinosawa <[email protected]>
  • Loading branch information
eryue0220 and kt3k committed Sep 19, 2024
1 parent ee9e302 commit 6a4eb6c
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 4 deletions.
13 changes: 13 additions & 0 deletions expect/_assertion.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions expect/_assertion_test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
21 changes: 21 additions & 0 deletions expect/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -216,6 +217,8 @@ export function expect<T extends Expected = Expected>(
} else {
matcher(context, ...args);
}

emitAssertionTrigger();
}

return isPromised
Expand Down Expand Up @@ -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;
8 changes: 4 additions & 4 deletions expect/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
111 changes: 111 additions & 0 deletions internal/assertion_state.ts
Original file line number Diff line number Diff line change
@@ -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;
}
28 changes: 28 additions & 0 deletions internal/assertion_state_test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
1 change: 1 addition & 0 deletions internal/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions internal/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
*
* @module
*/
export * from "./assertion_state.ts";
export * from "./build_message.ts";
export * from "./diff.ts";
export * from "./diff_str.ts";
Expand Down
9 changes: 9 additions & 0 deletions testing/bdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,8 @@
* @module
*/

import { getAssertionState } from "@std/internal/assertion-state";
import { AssertionError } from "@std/assert/assertion-error";
import {
type DescribeDefinition,
type HookNames,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 6a4eb6c

Please sign in to comment.