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(
+ `"