From 90ddec4503d7043bba3e7069a535294b4636cabd Mon Sep 17 00:00:00 2001 From: JDOM10 Date: Fri, 12 Jun 2026 17:01:53 -0500 Subject: [PATCH 1/3] feat(dom): add toContainHTML assertion method and corresponding tests --- packages/dom/src/lib/ElementAssertion.ts | 42 ++++++++++- .../test/unit/lib/ElementAssertion.test.tsx | 69 +++++++++++++++++++ .../lib/fixtures/containHtmlTestComponent.tsx | 12 ++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 21b1faf..76f9efa 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -2,7 +2,7 @@ import { Assertion, AssertionError } from "@assertive-ts/core"; import equal from "fast-deep-equal"; import { getAccessibleDescription, isValidAriaPressed } from "./helpers/accessibility"; -import { isButtonElement, isElementEmpty } from "./helpers/dom"; +import { isButtonElement, isElementEmpty} from "./helpers/dom"; import { getExpectedAndReceivedStyles } from "./helpers/styles"; export class ElementAssertion extends Assertion { @@ -434,6 +434,46 @@ export class ElementAssertion extends Assertion { }); } + /** + * Asserts that the element contains the specified HTML. + * + * @example + * ``` + * expect(container).toContainHTML('Hello'); + * expect(container).toContainHTML('
Bar
'); + * ``` + * + * @param htmlText The HTML text that should be contained in the element + * @returns the assertion instance. + */ + public toContainHTML(htmlText: string): this { + if (typeof htmlText !== "string") { + throw new Error(`.toContainHTML() expects a string value, got ${typeof htmlText}`); + } + + if (htmlText === "") { + throw new Error(".toContainHTML() expects a non-empty string"); + } + + const error = new AssertionError({ + actual: this.actual, + expected: htmlText, + message: `Expected the element to contain HTML: ${htmlText}` + }); + + const invertedError = new AssertionError({ + actual: this.actual, + expected: htmlText, + message: `Expected the element NOT to contain HTML: ${htmlText}` + }); + + return this.execute({ + assertWhen: this.actual.outerHTML.includes(htmlText), + error, + invertedError + }); + } + /** * Helper method to assert the presence or absence of class names. * diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index ec746b2..63472c5 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -8,6 +8,7 @@ import { NestedElementsTest } from "./fixtures/NestedElementsTest"; import { PressedTestComponent } from "./fixtures/PressedTestComponent"; import { SimpleTest } from "./fixtures/SimpleTest"; import { WithAttributesTest } from "./fixtures/WithAttributesTest"; +import { ContainHtmlTestComponent } from "./fixtures/containHtmlTestComponent"; import { DescriptionTestComponent } from "./fixtures/descriptionTestComponent"; import { FocusTestComponent } from "./fixtures/focusTestComponent"; @@ -823,4 +824,72 @@ describe("[Unit] ElementAssertion.test.ts", () => { }); }); }); + + describe(".toContainHTML", () => { + context("when the element contains the expected HTML", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const container = getByTestId("container"); + const test = new ElementAssertion(container); + + expect(test.toContainHTML('Hello World')).toBeEqual(test); + + expect(() => test.not.toContainHTML('Hello World')) + .toThrowError(AssertionError) + .toHaveMessage('Expected the element NOT to contain HTML: Hello World'); + }); + }); + + context("when the element contains nested HTML", () => { + it("returns the assertion instance", () => { + const { getByTestId } = render(); + const container = getByTestId("container"); + const test = new ElementAssertion(container); + + expect(test.toContainHTML("

Nested content

")).toBeEqual(test); + + expect(() => test.not.toContainHTML("

Nested content

")) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element NOT to contain HTML:

Nested content

"); + }); + }); + + context("when the element does not contain the expected HTML", () => { + it("throws an assertion error", () => { + const { getByTestId } = render(); + const container = getByTestId("container"); + const test = new ElementAssertion(container); + + expect(() => test.toContainHTML("
Not present
")) + .toThrowError(AssertionError) + .toHaveMessage("Expected the element to contain HTML:
Not present
"); + + expect(test.not.toContainHTML("
Not present
")).toBeEqual(test); + }); + }); + + context("when a non-string value is passed", () => { + it("throws an error", () => { + const { getByTestId } = render(); + const container = getByTestId("container"); + const test = new ElementAssertion(container); + + expect(() => test.toContainHTML(123 as unknown as string)) + .toThrowError(Error) + .toHaveMessage(".toContainHTML() expects a string value, got number"); + }); + }); + + context("when an empty string is passed", () => { + it("throws an error", () => { + const { getByTestId } = render(); + const container = getByTestId("container"); + const test = new ElementAssertion(container); + + expect(() => test.toContainHTML("")) + .toThrowError(Error) + .toHaveMessage(".toContainHTML() expects a non-empty string"); + }); + }); + }); }); diff --git a/packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx b/packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx new file mode 100644 index 0000000..444f879 --- /dev/null +++ b/packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx @@ -0,0 +1,12 @@ +import { ReactElement } from "react"; + +export function ContainHtmlTestComponent(): ReactElement { + return ( +
+ Hello World +
+

Nested content

+
+
+ ); +} From 96c7dea4956021c7361a5ce5c870d8b829562e7e Mon Sep 17 00:00:00 2001 From: JDOM10 Date: Fri, 12 Jun 2026 17:07:28 -0500 Subject: [PATCH 2/3] fix(dom): correct import statement and ensure string literals are wrapped in braces --- packages/dom/src/lib/ElementAssertion.ts | 8 ++++---- .../test/unit/lib/fixtures/containHtmlTestComponent.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 76f9efa..8e7692f 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -2,7 +2,7 @@ import { Assertion, AssertionError } from "@assertive-ts/core"; import equal from "fast-deep-equal"; import { getAccessibleDescription, isValidAriaPressed } from "./helpers/accessibility"; -import { isButtonElement, isElementEmpty} from "./helpers/dom"; +import { isButtonElement, isElementEmpty } from "./helpers/dom"; import { getExpectedAndReceivedStyles } from "./helpers/styles"; export class ElementAssertion extends Assertion { @@ -458,19 +458,19 @@ export class ElementAssertion extends Assertion { const error = new AssertionError({ actual: this.actual, expected: htmlText, - message: `Expected the element to contain HTML: ${htmlText}` + message: `Expected the element to contain HTML: ${htmlText}`, }); const invertedError = new AssertionError({ actual: this.actual, expected: htmlText, - message: `Expected the element NOT to contain HTML: ${htmlText}` + message: `Expected the element NOT to contain HTML: ${htmlText}`, }); return this.execute({ assertWhen: this.actual.outerHTML.includes(htmlText), error, - invertedError + invertedError, }); } diff --git a/packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx b/packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx index 444f879..6e07da3 100644 --- a/packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx +++ b/packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx @@ -1,11 +1,11 @@ -import { ReactElement } from "react"; +import type { ReactElement } from "react"; export function ContainHtmlTestComponent(): ReactElement { return (
- Hello World + {"Hello World"}
-

Nested content

+

{"Nested content"}

); From 3b9d68bf7f7c15142f6a6683fa6915669854794e Mon Sep 17 00:00:00 2001 From: JDOM10 Date: Tue, 30 Jun 2026 08:38:09 -0500 Subject: [PATCH 3/3] feat(dom): add HTML normalization in toContainHTML assertion and corresponding tests --- packages/dom/src/lib/ElementAssertion.ts | 11 ++++++++--- packages/dom/src/lib/helpers/dom.ts | 7 +++++++ packages/dom/test/unit/lib/ElementAssertion.test.tsx | 12 ++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 8e7692f..8207cc9 100644 --- a/packages/dom/src/lib/ElementAssertion.ts +++ b/packages/dom/src/lib/ElementAssertion.ts @@ -2,7 +2,7 @@ import { Assertion, AssertionError } from "@assertive-ts/core"; import equal from "fast-deep-equal"; import { getAccessibleDescription, isValidAriaPressed } from "./helpers/accessibility"; -import { isButtonElement, isElementEmpty } from "./helpers/dom"; +import { isButtonElement, isElementEmpty, normalizeHtml } from "./helpers/dom"; import { getExpectedAndReceivedStyles } from "./helpers/styles"; export class ElementAssertion extends Assertion { @@ -437,10 +437,15 @@ export class ElementAssertion extends Assertion { /** * Asserts that the element contains the specified HTML. * + * The expected HTML is normalized through a detached element before + * comparison, so differences in attribute quoting (single vs double quotes), + * tag case, and whitespace between attributes are ignored. Attribute ordering + * and whitespace within text content are still significant. + * * @example * ``` * expect(container).toContainHTML('Hello'); - * expect(container).toContainHTML('
Bar
'); + * expect(container).toContainHTML("
Bar
"); * ``` * * @param htmlText The HTML text that should be contained in the element @@ -468,7 +473,7 @@ export class ElementAssertion extends Assertion { }); return this.execute({ - assertWhen: this.actual.outerHTML.includes(htmlText), + assertWhen: this.actual.outerHTML.includes(normalizeHtml(htmlText, this.actual.ownerDocument)), error, invertedError, }); diff --git a/packages/dom/src/lib/helpers/dom.ts b/packages/dom/src/lib/helpers/dom.ts index 3e1fb90..7850bc8 100644 --- a/packages/dom/src/lib/helpers/dom.ts +++ b/packages/dom/src/lib/helpers/dom.ts @@ -5,6 +5,13 @@ export function isElementEmpty(element: Element): boolean { return nonCommentChildNodes.length === 0; } +export function normalizeHtml(htmlText: string, ownerDocument: Document): string { + const div = ownerDocument.createElement("div"); + div.innerHTML = htmlText; + + return div.innerHTML; +} + export function isButtonElement(element: Element): boolean { const roles = (element.getAttribute("role") || "") .split(" ") diff --git a/packages/dom/test/unit/lib/ElementAssertion.test.tsx b/packages/dom/test/unit/lib/ElementAssertion.test.tsx index 63472c5..4b101a0 100644 --- a/packages/dom/test/unit/lib/ElementAssertion.test.tsx +++ b/packages/dom/test/unit/lib/ElementAssertion.test.tsx @@ -854,6 +854,18 @@ describe("[Unit] ElementAssertion.test.ts", () => { }); }); + context("when the expected HTML differs only in formatting", () => { + it("normalizes quotes and tag case before comparing", () => { + const { getByTestId } = render(); + const container = getByTestId("container"); + const test = new ElementAssertion(container); + + expect(test.toContainHTML("Hello World")).toBeEqual(test); + expect(test.toContainHTML('Hello World')).toBeEqual(test); + expect(test.toContainHTML("

Nested content

")).toBeEqual(test); + }); + }); + context("when the element does not contain the expected HTML", () => { it("throws an assertion error", () => { const { getByTestId } = render();