diff --git a/src/lib/components/portal/portal.test.ts b/src/lib/components/portal/portal.test.ts new file mode 100644 index 0000000..68abd1a --- /dev/null +++ b/src/lib/components/portal/portal.test.ts @@ -0,0 +1,295 @@ +import svelte from "svelte-inline-compile"; +import { render } from "@testing-library/svelte"; +import Portal from "./Portal.svelte"; +import PortalGroup from "./PortalGroup.svelte"; +import { click } from "$lib/test-utils/interactions"; + +function getPortalRoot() { + return document.getElementById('headlessui-portal-root')! +} + +beforeEach(() => { + document.body.innerHTML = '' +}) + +it('should be possible to use a Portal', () => { + // Dummy assertion to trick TS compiler + expect(Portal).not.toBe(PortalGroup); + + expect(getPortalRoot()).toBe(null) + + render(svelte` +
+ +

Contents...

+
+
+ `) + + let parent = document.getElementById('parent') + let content = document.getElementById('content') + + expect(getPortalRoot()).not.toBe(null) + + // Ensure the content is not part of the parent + expect(parent).not.toContainElement(content) + + // Ensure the content does exist + expect(content).not.toBe(null) + expect(content).toHaveTextContent('Contents...') +}) + +it('should be possible to use multiple Portal elements', () => { + expect(getPortalRoot()).toBe(null) + + render(svelte` +
+ +

Contents 1 ...

+
+
+ +

Contents 2 ...

+
+
+ `) + + let parent = document.getElementById('parent') + let content1 = document.getElementById('content1') + let content2 = document.getElementById('content2') + + expect(getPortalRoot()).not.toBe(null) + + // Ensure the content1 is not part of the parent + expect(parent).not.toContainElement(content1) + + // Ensure the content2 is not part of the parent + expect(parent).not.toContainElement(content2) + + // Ensure the content does exist + expect(content1).not.toBe(null) + expect(content1).toHaveTextContent('Contents 1 ...') + + // Ensure the content does exist + expect(content2).not.toBe(null) + expect(content2).toHaveTextContent('Contents 2 ...') +}) + +it('should cleanup the Portal root when the last Portal is unmounted', async () => { + expect(getPortalRoot()).toBe(null) + + render(svelte` + +
+ + + + {#if renderA} + +

Contents 1 ...

+
+ {/if} + + {#if renderB} + +

Contents 2 ...

+
+ {/if} +
+ `) + + let a = document.getElementById('a') + let b = document.getElementById('b') + + expect(getPortalRoot()).toBe(null) + + // Let's render the first Portal + await click(a) + + expect(getPortalRoot()).not.toBe(null) + expect(getPortalRoot().childNodes).toHaveLength(1) + + // Let's render the second Portal + await click(b) + + expect(getPortalRoot()).not.toBe(null) + expect(getPortalRoot().childNodes).toHaveLength(2) + + // Let's remove the first portal + await click(a) + + expect(getPortalRoot()).not.toBe(null) + expect(getPortalRoot().childNodes).toHaveLength(1) + + // Let's remove the second Portal + await click(b) + + expect(getPortalRoot()).toBe(null) + + // Let's render the first Portal again + await click(a) + + expect(getPortalRoot()).not.toBe(null) + expect(getPortalRoot().childNodes).toHaveLength(1) +}) + +it('should be possible to render multiple portals at the same time', async () => { + expect(getPortalRoot()).toBe(null) + + render(svelte` + +
+ + + + + + + {#if renderA} + +

Contents 1 ...

+
+ {/if} + + {#if renderB} + +

Contents 2 ...

+
+ {/if} + + {#if renderC} + +

Contents 3 ...

+
+ {/if} +
+ `) + + expect(getPortalRoot()).not.toBe(null) + expect(getPortalRoot().childNodes).toHaveLength(3) + + // Remove Portal 1 + await click(document.getElementById('a')) + expect(getPortalRoot().childNodes).toHaveLength(2) + + // Remove Portal 2 + await click(document.getElementById('b')) + expect(getPortalRoot().childNodes).toHaveLength(1) + + // Re-add Portal 1 + await click(document.getElementById('a')) + expect(getPortalRoot().childNodes).toHaveLength(2) + + // Remove Portal 3 + await click(document.getElementById('c')) + expect(getPortalRoot().childNodes).toHaveLength(1) + + // Remove Portal 1 + await click(document.getElementById('a')) + expect(getPortalRoot()).toBe(null) + + // Render A and B at the same time! + await click(document.getElementById('double')) + expect(getPortalRoot().childNodes).toHaveLength(2) +}) + +it('should be possible to tamper with the modal root and restore correctly', async () => { + expect(getPortalRoot()).toBe(null) + + render(svelte` + +
+ + + + {#if renderA} + +

Contents 1 ...

+
+ {/if} + + {#if renderB} + +

Contents 2 ...

+
+ {/if} +
+ `) + + expect(getPortalRoot()).not.toBe(null) + + // Tamper tamper + document.body.removeChild(document.getElementById('headlessui-portal-root')!) + + // Hide Portal 1 and 2 + await click(document.getElementById('a')) + await click(document.getElementById('b')) + + expect(getPortalRoot()).toBe(null) + + // Re-show Portal 1 and 2 + await click(document.getElementById('a')) + await click(document.getElementById('b')) + + expect(getPortalRoot()).not.toBe(null) + expect(getPortalRoot().childNodes).toHaveLength(2) +}) + +it('should be possible to force the Portal into a specific element using PortalGroup', async () => { + render(svelte` + +
+ + + +
+ B +
+ Next to A +
+ + I am in the portal root +
+ `) + + // The random whitespace in here is a little annoying but whatever + expect(document.body.innerHTML).toMatchInlineSnapshot( + `"
B
I am in the portal root
"` + ) +})