diff --git a/packages/dom/src/lib/ElementAssertion.ts b/packages/dom/src/lib/ElementAssertion.ts index 21b1faf..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 { @@ -434,6 +434,51 @@ 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
"); + * ``` + * + * @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(normalizeHtml(htmlText, this.actual.ownerDocument)), + error, + invertedError, + }); + } + /** * Helper method to assert the presence or absence of class names. * 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 ec746b2..4b101a0 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,84 @@ 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 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(); + 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..6e07da3 --- /dev/null +++ b/packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx @@ -0,0 +1,12 @@ +import type { ReactElement } from "react"; + +export function ContainHtmlTestComponent(): ReactElement { + return ( +
+ {"Hello World"} +
+

{"Nested content"}

+
+
+ ); +}