Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion packages/dom/src/lib/ElementAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Element> extends Assertion<T> {
Expand Down Expand Up @@ -434,6 +434,51 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
});
}

/**
* 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('<span>Hello</span>');
* expect(container).toContainHTML("<div class='foo'>Bar</div>");
* ```
*
* @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)),

@KeylaMunnoz KeylaMunnoz Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @JDOM10 ! Great progress here! I just have one small follow-up comment.

I feel like right now the comparison is asymmetric because the expected HTML goes through normalizeHtml, but this.actual.outerHTML is compared raw. So, both sides are not processed by the same rules, which could cause subtle mismatches in environments where outerHTML serializes differently than JSDOM does (e.g. a real browser).

So, to fix this I would recommend normalizing both sides before comparing:

normalizeHtml(this.actual.outerHTML, this.actual.ownerDocument)
  .includes(normalizeHtml(htmlText, this.actual.ownerDocument))

I think this would ensure the same rules are applied to both the actual element and the expected HTML string, making the comparison truly symmetric. Let me know what you think :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @KeylaMunnoz !

Wrapping outerHTML in normalizeHtml just re-cleans something already clean → same result, extra work. And the "real browser" worry doesn't happen because normalizeHtml uses this.actual.ownerDocument — the element's own document. So it's always the same environment on both sides; they can't diverge.

error,
invertedError,
});
}

/**
* Helper method to assert the presence or absence of class names.
*
Expand Down
7 changes: 7 additions & 0 deletions packages/dom/src/lib/helpers/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(" ")
Expand Down
81 changes: 81 additions & 0 deletions packages/dom/test/unit/lib/ElementAssertion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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(<ContainHtmlTestComponent />);
const container = getByTestId("container");
const test = new ElementAssertion(container);

expect(test.toContainHTML('<span data-testid="child-span">Hello World</span>')).toBeEqual(test);

expect(() => test.not.toContainHTML('<span data-testid="child-span">Hello World</span>'))
.toThrowError(AssertionError)
.toHaveMessage('Expected the element NOT to contain HTML: <span data-testid="child-span">Hello World</span>');
});
});

context("when the element contains nested HTML", () => {
it("returns the assertion instance", () => {
const { getByTestId } = render(<ContainHtmlTestComponent />);
const container = getByTestId("container");
const test = new ElementAssertion(container);

expect(test.toContainHTML("<p>Nested content</p>")).toBeEqual(test);

expect(() => test.not.toContainHTML("<p>Nested content</p>"))
.toThrowError(AssertionError)
.toHaveMessage("Expected the element NOT to contain HTML: <p>Nested content</p>");
});
});

context("when the expected HTML differs only in formatting", () => {
it("normalizes quotes and tag case before comparing", () => {
const { getByTestId } = render(<ContainHtmlTestComponent />);
const container = getByTestId("container");
const test = new ElementAssertion(container);

expect(test.toContainHTML("<span data-testid='child-span'>Hello World</span>")).toBeEqual(test);
expect(test.toContainHTML('<SPAN data-testid="child-span">Hello World</SPAN>')).toBeEqual(test);
expect(test.toContainHTML("<div class='nested'><p>Nested content</p></div>")).toBeEqual(test);
});
});

context("when the element does not contain the expected HTML", () => {
it("throws an assertion error", () => {
const { getByTestId } = render(<ContainHtmlTestComponent />);
const container = getByTestId("container");
const test = new ElementAssertion(container);

expect(() => test.toContainHTML("<div>Not present</div>"))
.toThrowError(AssertionError)
.toHaveMessage("Expected the element to contain HTML: <div>Not present</div>");

expect(test.not.toContainHTML("<div>Not present</div>")).toBeEqual(test);
});
});

context("when a non-string value is passed", () => {
it("throws an error", () => {
const { getByTestId } = render(<ContainHtmlTestComponent />);
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(<ContainHtmlTestComponent />);
const container = getByTestId("container");
const test = new ElementAssertion(container);

expect(() => test.toContainHTML(""))
.toThrowError(Error)
.toHaveMessage(".toContainHTML() expects a non-empty string");
});
});
});
});
12 changes: 12 additions & 0 deletions packages/dom/test/unit/lib/fixtures/containHtmlTestComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ReactElement } from "react";

export function ContainHtmlTestComponent(): ReactElement {
return (
<div data-testid="container">
<span data-testid="child-span">{"Hello World"}</span>
<div className="nested">
<p>{"Nested content"}</p>
</div>
</div>
);
}
Loading