import { cleanup, render } from "@testing-library/react";
import React from "react";
import Portal from "../portal";
describe("Portal", () => {
afterEach(() => {
const portals = document.querySelectorAll('[id^="test-portal"]');
portals.forEach((portal) => portal.remove());
cleanup();
});
it("renders children into a portal", () => {
const { container } = render(
Portal Content
,
);
expect(
container.querySelector('[data-testid="portal-content"]'),
).toBeNull();
const portalRoot = document.getElementById("test-portal-1");
expect(portalRoot).toBeTruthy();
expect(
portalRoot?.querySelector('[data-testid="portal-content"]'),
).toBeTruthy();
});
it("creates portal root if it doesn't exist", () => {
expect(document.getElementById("test-portal-2")).toBeNull();
render(
Content
,
);
const portalRoot = document.getElementById("test-portal-2");
expect(portalRoot).not.toBeNull();
expect(portalRoot?.parentElement).toBe(document.body);
});
it("uses existing portal root if it exists", () => {
const existingRoot = document.createElement("div");
existingRoot.id = "test-portal-3";
document.body.appendChild(existingRoot);
render(
Using Existing
,
);
const portalRoot = document.getElementById("test-portal-3");
expect(portalRoot).toBe(existingRoot);
expect(
portalRoot?.querySelector('[data-testid="existing-content"]'),
).toBeTruthy();
existingRoot.remove();
});
it("removes portal content on unmount", () => {
const { unmount } = render(
Cleanup Test
,
);
const portalRoot = document.getElementById("test-portal-4");
expect(
portalRoot?.querySelector('[data-testid="portal-content"]'),
).toBeTruthy();
unmount();
const stillExists = document.getElementById("test-portal-4");
expect(stillExists).toBeTruthy();
expect(
stillExists?.querySelector('[data-testid="portal-content"]'),
).toBeNull();
});
it("renders multiple children correctly", () => {
render(
Child 1
Child 2
Child 3
,
);
const portalRoot = document.getElementById("test-portal-5");
expect(portalRoot?.querySelector('[data-testid="child-1"]')).toBeTruthy();
expect(portalRoot?.querySelector('[data-testid="child-2"]')).toBeTruthy();
expect(portalRoot?.querySelector('[data-testid="child-3"]')).toBeTruthy();
});
it("handles multiple portals", () => {
render(
Portal A
,
);
render(
Portal B
,
);
expect(document.getElementById("test-portal-6a")).not.toBeNull();
expect(document.getElementById("test-portal-6b")).not.toBeNull();
});
it("works with shadow DOM when portalHost is provided", () => {
const shadowHost = document.createElement("div");
document.body.appendChild(shadowHost);
const shadowRoot = shadowHost.attachShadow({ mode: "open" });
render(
Shadow Content
,
);
const portalRoot = shadowRoot.getElementById("test-portal-shadow");
expect(portalRoot).toBeTruthy();
expect(
portalRoot?.querySelector('[data-testid="shadow-content"]'),
).toBeTruthy();
shadowHost.remove();
});
it("appends to portalHost instead of document.body when provided", () => {
const customHost = document.createElement("div");
document.body.appendChild(customHost);
const shadowRoot = customHost.attachShadow({ mode: "open" });
render(
Custom Host Content
,
);
const portalRoot = shadowRoot.getElementById("test-portal-custom-host");
expect(portalRoot).toBeTruthy();
expect(portalRoot?.parentNode).toBe(shadowRoot);
customHost.remove();
});
it("creates portal root in portalHost when it doesn't exist", () => {
const shadowHost = document.createElement("div");
document.body.appendChild(shadowHost);
const shadowRoot = shadowHost.attachShadow({ mode: "open" });
render(
Shadow Portal
,
);
const portalRoot = shadowRoot.getElementById("test-portal-7");
expect(portalRoot).not.toBeNull();
expect(shadowRoot.contains(portalRoot!)).toBe(true);
shadowHost.remove();
});
it("handles re-renders correctly", () => {
const { rerender } = render(
Content 1
,
);
const portalRoot = document.getElementById("test-portal-8");
expect(portalRoot?.querySelector('[data-testid="content-1"]')).toBeTruthy();
rerender(
Content 2
,
);
expect(portalRoot?.querySelector('[data-testid="content-1"]')).toBeNull();
expect(portalRoot?.querySelector('[data-testid="content-2"]')).toBeTruthy();
});
});