diff --git a/expect/_assertions_test.ts b/expect/_assertions_test.ts index f59764cbeb5a..6e33fe4abc44 100644 --- a/expect/_assertions_test.ts +++ b/expect/_assertions_test.ts @@ -1,64 +1,180 @@ // Copyright 2018-2025 the Deno authors. 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 through `toThrowErrorMatchingSnapshot` - it("should throw an error", () => { - expect.hasAssertions(); - }); +import * as path from "@std/path"; +import { assertStringIncludes } from "@std/assert"; +import { stripAnsiCode } from "@std/internal/styles"; - it("should pass", () => { - expect.hasAssertions(); - expect("a").toEqual("a"); - }); - }); - - it("it() suite should pass", () => { - expect.hasAssertions(); - expect("a").toEqual("a"); +Deno.test("expect.hasAssertions() API", async () => { + const tempDirPath = await Deno.makeTempDir({ + prefix: "deno_std_has_assertions_", }); + try { + const tempFilePath = path.join(tempDirPath, "has_assertions_test.ts"); + await Deno.writeTextFile( + tempFilePath, + `import { describe, it, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; - // FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot` - test("test suite should throw an error", () => { +describe("describe suite", () => { + it("describe should throw an error", () => { expect.hasAssertions(); }); - test("test suite should pass", () => { + it("describe should pass", () => { expect.hasAssertions(); expect("a").toEqual("a"); }); }); -Deno.test("expect.assertions() API", () => { - test("should pass", () => { - expect.assertions(2); - expect("a").not.toBe("b"); - expect("a").toBe("a"); - }); +it("it() suite should throw an error", () => { + expect.hasAssertions(); +}); - // FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot` - test("should throw error", () => { - expect.assertions(1); - expect("a").not.toBe("b"); - expect("a").toBe("a"); - }); +it("it() suite should pass", () => { + expect.hasAssertions(); + expect("a").toEqual("a"); +}); - it("redeclare different assertion count", () => { - expect.assertions(3); - expect("a").not.toBe("b"); - expect("a").toBe("a"); - expect.assertions(2); - }); +test("test suite should throw an error", () => { + expect.hasAssertions(); +}); + +test("test suite should pass", () => { + expect.hasAssertions(); + expect("a").toEqual("a"); +}); +`, + ); + + const command = new Deno.Command(Deno.execPath(), { + args: ["test", "--no-lock", tempDirPath], + }); + const { stdout } = await command.output(); + const errorMessage = stripAnsiCode(new TextDecoder().decode(stdout)); + + assertStringIncludes( + errorMessage, + "describe should throw an error ... FAILED", + ); + assertStringIncludes(errorMessage, "describe should pass ... ok"); + assertStringIncludes( + errorMessage, + "it() suite should throw an error ... FAILED", + ); + assertStringIncludes(errorMessage, "it() suite should pass ... ok"); + assertStringIncludes( + errorMessage, + "test suite should throw an error ... FAILED", + ); + assertStringIncludes(errorMessage, "test suite should pass ... ok"); - test("expect no assertions", () => { - expect.assertions(0); + assertStringIncludes( + errorMessage, + "error: AssertionError: Expected at least one assertion to be called but received none", + ); + } finally { + await Deno.remove(tempDirPath, { recursive: true }); + } +}); + +Deno.test("expect.assertions() API", async () => { + const tempDirPath = await Deno.makeTempDir({ + prefix: "deno_std_has_assertions_", }); + try { + const tempFilePath = path.join(tempDirPath, "has_assertions_test.ts"); + await Deno.writeTextFile( + tempFilePath, + `import { describe, it, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; - // FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot` - it("should throw an error", () => { - expect.assertions(2); +test("should pass", () => { + expect.assertions(2); + expect("a").not.toBe("b"); + expect("a").toBe("a"); +}); + +test("should throw error", () => { + expect.assertions(1); + expect("a").not.toBe("b"); + expect("a").toBe("a"); +}); + +it("redeclare different assertion count", () => { + expect.assertions(3); + expect("a").not.toBe("b"); + expect("a").toBe("a"); + expect.assertions(2); +}); + +test("expect no assertions", () => { + expect.assertions(0); +}); + +it("should throw an error", () => { + expect.assertions(2); +}); +`, + ); + + const command = new Deno.Command(Deno.execPath(), { + args: ["test", "--no-lock", tempDirPath], + }); + const { stdout } = await command.output(); + const errorMessage = stripAnsiCode(new TextDecoder().decode(stdout)); + + assertStringIncludes(errorMessage, "should pass ... ok"); + assertStringIncludes(errorMessage, "should throw error ... FAILED"); + assertStringIncludes( + errorMessage, + "redeclare different assertion count ... ok", + ); + assertStringIncludes(errorMessage, "expect no assertions ... ok"); + assertStringIncludes(errorMessage, "should throw an error ... FAILED"); + + assertStringIncludes( + errorMessage, + "error: AssertionError: Expected exactly 1 assertion to be called, but received 2", + ); + assertStringIncludes( + errorMessage, + "error: AssertionError: Expected exactly 2 assertions to be called, but received 0", + ); + } finally { + await Deno.remove(tempDirPath, { recursive: true }); + } +}); + +Deno.test("expect assertions reset after errored tests", async () => { + const tempDirPath = await Deno.makeTempDir({ + prefix: "deno_std_assertions_reset_", }); + try { + const tempFilePath = path.join(tempDirPath, "assertion_reset_test.ts"); + await Deno.writeTextFile( + tempFilePath, + `import { it } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +it("should fail", () => { + expect.assertions(2); + expect(1).toBe(1); +}); +it("should pass", () => { + expect.assertions(0); +}); +`, + ); + + const command = new Deno.Command(Deno.execPath(), { + args: ["test", "--no-lock", tempDirPath], + }); + const { stdout } = await command.output(); + const errorMessage = stripAnsiCode(new TextDecoder().decode(stdout)); + + assertStringIncludes(errorMessage, "should fail ... FAILED"); + // Previously "should fail" failing caused "should pass" to fail + assertStringIncludes(errorMessage, "should pass ... ok"); + } finally { + await Deno.remove(tempDirPath, { recursive: true }); + } }); diff --git a/internal/assertion_state.ts b/internal/assertion_state.ts index 6a1b79ab6632..3e29925a4b31 100644 --- a/internal/assertion_state.ts +++ b/internal/assertion_state.ts @@ -26,6 +26,20 @@ export class AssertionState { assertionTriggered: false, assertionTriggeredCount: 0, }; + + // If any checks were registered, after the test suite runs the checks, + // `resetAssertionState` should also have been called. If it was not, + // then the test suite did not run the checks. + globalThis.addEventListener("unload", () => { + if ( + this.#state.assertionCheck || + this.#state.assertionCount !== undefined + ) { + throw new Error( + "AssertionCounter was not cleaned up: If tests are not otherwise failing, ensure `expect.hasAssertion` and `expect.assertions` are only run in bdd tests", + ); + } + }); } /** diff --git a/internal/assertion_state_test.ts b/internal/assertion_state_test.ts index ad3148cddd1f..e3453f83916d 100644 --- a/internal/assertion_state_test.ts +++ b/internal/assertion_state_test.ts @@ -1,28 +1,56 @@ // Copyright 2018-2025 the Deno authors. MIT license. -import { assertEquals } from "@std/assert"; +import { assertEquals, assertStringIncludes } from "@std/assert"; import { AssertionState } from "./assertion_state.ts"; +import { stripAnsiCode } from "./styles.ts"; Deno.test("AssertionState checkAssertionErrorState pass", () => { const assertionState = new AssertionState(); - assertionState.setAssertionTriggered(true); - - assertEquals(assertionState.checkAssertionErrorState(), false); + try { + assertionState.setAssertionTriggered(true); + assertEquals(assertionState.checkAssertionErrorState(), false); + } finally { + assertionState.resetAssertionState(); + } }); Deno.test("AssertionState checkAssertionErrorState pass", () => { const assertionState = new AssertionState(); - assertionState.setAssertionTriggered(true); + try { + assertionState.setAssertionTriggered(true); - assertEquals(assertionState.checkAssertionErrorState(), false); + assertEquals(assertionState.checkAssertionErrorState(), false); - assertionState.setAssertionCheck(true); - assertEquals(assertionState.checkAssertionErrorState(), false); + assertionState.setAssertionCheck(true); + assertEquals(assertionState.checkAssertionErrorState(), false); + } finally { + assertionState.resetAssertionState(); + } }); Deno.test("AssertionState checkAssertionErrorState fail", () => { const assertionState = new AssertionState(); - assertionState.setAssertionCheck(true); + try { + assertionState.setAssertionCheck(true); + assertEquals(assertionState.checkAssertionErrorState(), true); + } finally { + assertionState.resetAssertionState(); + } +}); - assertEquals(assertionState.checkAssertionErrorState(), true); +Deno.test("AssertionState throws if not cleaned up", async () => { + const command = new Deno.Command(Deno.execPath(), { + args: [ + "eval", + ` + import { AssertionState } from "@std/internal/assertion-state"; + const assertionState = new AssertionState(); + assertionState.setAssertionCount(0); + `, + ], + }); + const { stderr } = await command.output(); + const errorMessage = stripAnsiCode(new TextDecoder().decode(stderr)); + // TODO(WWRS): Test for the expected message when Deno displays it instead of "Uncaught null" + assertStringIncludes(errorMessage, "error: Uncaught"); }); diff --git a/testing/_test_suite.ts b/testing/_test_suite.ts index 055bdd428740..b2cf1b60b926 100644 --- a/testing/_test_suite.ts +++ b/testing/_test_suite.ts @@ -429,19 +429,23 @@ export class TestSuiteInternal implements TestSuite { await fn.call(context, t); } - if (assertionState.checkAssertionErrorState()) { - throw new AssertionError( - "Expected at least one assertion to be called but received none", - ); - } + try { + if (assertionState.checkAssertionErrorState()) { + throw new AssertionError( + "Expected at least one assertion to be called but received none", + ); + } - if (assertionState.checkAssertionCountSatisfied()) { - throw new AssertionError( - `Expected at least ${assertionState.assertionCount} assertion to be called, ` + - `but received ${assertionState.assertionTriggeredCount}`, - ); + if (assertionState.checkAssertionCountSatisfied()) { + throw new AssertionError( + `Expected exactly ${assertionState.assertionCount} ${ + assertionState.assertionCount === 1 ? "assertion" : "assertions" + } to be called, ` + + `but received ${assertionState.assertionTriggeredCount}`, + ); + } + } finally { + assertionState.resetAssertionState(); } - - assertionState.resetAssertionState(); } } diff --git a/testing/bdd.ts b/testing/bdd.ts index f14efbd65832..3f23edc294ec 100644 --- a/testing/bdd.ts +++ b/testing/bdd.ts @@ -600,20 +600,24 @@ export function it(...args: ItArgs) { TestSuiteInternal.runningCount--; } - if (assertionState.checkAssertionErrorState()) { - throw new AssertionError( - "Expected at least one assertion to be called but received none", - ); - } + try { + if (assertionState.checkAssertionErrorState()) { + throw new AssertionError( + "Expected at least one assertion to be called but received none", + ); + } - if (assertionState.checkAssertionCountSatisfied()) { - throw new AssertionError( - `Expected at least ${assertionState.assertionCount} assertion to be called, ` + - `but received ${assertionState.assertionTriggeredCount}`, - ); + if (assertionState.checkAssertionCountSatisfied()) { + throw new AssertionError( + `Expected exactly ${assertionState.assertionCount} ${ + assertionState.assertionCount === 1 ? "assertion" : "assertions" + } to be called, ` + + `but received ${assertionState.assertionTriggeredCount}`, + ); + } + } finally { + assertionState.resetAssertionState(); } - - assertionState.resetAssertionState(); }, }; if (ignore !== undefined) {