Add initial batch of Dialog tests
This commit is contained in:
24
src/lib/components/dialog/_ManagedDialog.svelte
Normal file
24
src/lib/components/dialog/_ManagedDialog.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import Dialog from "./Dialog.svelte";
|
||||
|
||||
// This component is only for use in tests
|
||||
export let initialOpen = false;
|
||||
export let onClose = () => {};
|
||||
export let buttonText: string | null = null;
|
||||
export let buttonProps = {};
|
||||
let state = initialOpen;
|
||||
</script>
|
||||
|
||||
{#if buttonText !== null}
|
||||
<button {...buttonProps} on:click={() => (state = !state)}
|
||||
>{buttonText}</button
|
||||
>
|
||||
{/if}
|
||||
<Dialog
|
||||
{...$$restProps}
|
||||
open={state}
|
||||
on:close={(e) => (state = e.detail)}
|
||||
on:close={onClose}
|
||||
>
|
||||
<slot />
|
||||
</Dialog>
|
||||
2
src/lib/components/dialog/_TestTabSentinel.svelte
Normal file
2
src/lib/components/dialog/_TestTabSentinel.svelte
Normal file
@@ -0,0 +1,2 @@
|
||||
<div tabindex={0} {...$$restProps} on:focus />
|
||||
<slot />
|
||||
358
src/lib/components/dialog/dialog.test.ts
Normal file
358
src/lib/components/dialog/dialog.test.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { Dialog, DialogDescription, DialogOverlay, DialogTitle } from "."
|
||||
import TestTabSentinel from "./_TestTabSentinel.svelte";
|
||||
import ManagedDialog from "./_ManagedDialog.svelte";
|
||||
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
|
||||
import { render } from "@testing-library/svelte";
|
||||
import TestRenderer from "$lib/test-utils/TestRenderer.svelte";
|
||||
import Button from "$lib/internal/elements/Button.svelte";
|
||||
import P from "$lib/internal/elements/P.svelte";
|
||||
import Input from "$lib/internal/elements/Input.svelte";
|
||||
import { assertDialog, assertDialogDescription, DialogState, getDialog } from "$lib/test-utils/accessibility-assertions";
|
||||
import { click, Keys, press } from "$lib/test-utils/interactions";
|
||||
import Transition from "$lib/components/transitions/TransitionRoot.svelte";
|
||||
import { tick } from "svelte";
|
||||
|
||||
jest.mock('../../hooks/use-id')
|
||||
|
||||
// @ts-expect-error
|
||||
global.IntersectionObserver = class FakeIntersectionObserver {
|
||||
observe() { }
|
||||
disconnect() { }
|
||||
}
|
||||
|
||||
afterAll(() => jest.restoreAllMocks())
|
||||
|
||||
describe('Safe guards', () => {
|
||||
it.each([
|
||||
['DialogOverlay', DialogOverlay],
|
||||
['DialogTitle', DialogTitle],
|
||||
])(
|
||||
'should error when we are using a <%s /> without a parent <Dialog />',
|
||||
suppressConsoleLogs((name, Component) => {
|
||||
expect(() => render(Component)).toThrowError(
|
||||
`<${name} /> is missing a parent <Dialog /> component.`
|
||||
)
|
||||
expect.hasAssertions()
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to render a Dialog without crashing',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
TestRenderer, {
|
||||
allProps: [
|
||||
Dialog,
|
||||
{ open: false, onClose: console.log },
|
||||
[
|
||||
[Button,
|
||||
{},
|
||||
"Trigger"],
|
||||
[DialogOverlay],
|
||||
[DialogTitle],
|
||||
[P, {}, "Contents"],
|
||||
[DialogDescription]
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
assertDialog({
|
||||
state: DialogState.InvisibleUnmounted,
|
||||
attributes: { id: 'headlessui-dialog-1' },
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
describe('Dialog', () => {
|
||||
it(
|
||||
'should complain when the `open` and `onClose` prop are missing',
|
||||
suppressConsoleLogs(async () => {
|
||||
expect(() => render(Dialog, { as: "div" })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"You forgot to provide an \`open\` prop to the \`Dialog\` component."`
|
||||
)
|
||||
expect.hasAssertions()
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should complain when an `open` prop is not a boolean',
|
||||
suppressConsoleLogs(async () => {
|
||||
expect(() =>
|
||||
render(
|
||||
TestRenderer, {
|
||||
allProps: [
|
||||
Dialog,
|
||||
{ open: null, onClose: console.log, as: "div" },
|
||||
]
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: null"`
|
||||
)
|
||||
expect.hasAssertions()
|
||||
})
|
||||
)
|
||||
|
||||
// TODO: render prop tests!
|
||||
|
||||
// it(
|
||||
// 'should be possible to render a Dialog using a render prop',
|
||||
// suppressConsoleLogs(async () => {
|
||||
// function Example() {
|
||||
// let [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <button id= "trigger" onClick = {() => setIsOpen(true)
|
||||
// }>
|
||||
// Trigger
|
||||
// < /button>
|
||||
// < Dialog open = { isOpen } onClose = { setIsOpen } >
|
||||
// { data => (
|
||||
// <>
|
||||
// <pre>{ JSON.stringify(data) } < /pre>
|
||||
// < TabSentinel />
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
// </Dialog>
|
||||
// < />
|
||||
// )
|
||||
// }
|
||||
// render(<Example />)
|
||||
|
||||
// assertDialog({ state: DialogState.InvisibleUnmounted })
|
||||
|
||||
// await click(document.getElementById('trigger'))
|
||||
|
||||
// assertDialog({ state: DialogState.Visible, textContent: JSON.stringify({ open: true }) })
|
||||
// })
|
||||
// )
|
||||
|
||||
it('should be possible to always render the Dialog if we provide it a `static` prop (and enable focus trapping based on `open`)', async () => {
|
||||
let focusCounter = jest.fn()
|
||||
render(
|
||||
TestRenderer, {
|
||||
allProps: [
|
||||
[Button, {}, "Trigger"],
|
||||
[Dialog, { open: true, onClose: console.log, static: true }, [
|
||||
[P, {}, "Contents"],
|
||||
[TestTabSentinel, { onFocus: focusCounter }]
|
||||
]],
|
||||
]
|
||||
})
|
||||
|
||||
// Wait for the focus to take effect
|
||||
await tick();
|
||||
// Let's verify that the Dialog is already there
|
||||
expect(getDialog()).not.toBe(null)
|
||||
expect(focusCounter).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should be possible to always render the Dialog if we provide it a `static` prop (and disable focus trapping based on `open`)', () => {
|
||||
let focusCounter = jest.fn()
|
||||
render(
|
||||
TestRenderer, {
|
||||
allProps: [
|
||||
[Button, {}, "Trigger"],
|
||||
[Dialog, { open: false, onClose: console.log, static: true }, [
|
||||
[P, {}, "Contents"],
|
||||
[TestTabSentinel, { onFocus: focusCounter }]
|
||||
]],
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
// Let's verify that the Dialog is already there
|
||||
expect(getDialog()).not.toBe(null)
|
||||
expect(focusCounter).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should be possible to use a different render strategy for the Dialog', async () => {
|
||||
let focusCounter = jest.fn()
|
||||
render(
|
||||
TestRenderer, {
|
||||
allProps: [
|
||||
[ManagedDialog, { unmount: false, buttonText: "Trigger", buttonProps: { id: "trigger" } }, [
|
||||
[Input, { onFocus: focusCounter }],
|
||||
]],
|
||||
]
|
||||
})
|
||||
|
||||
assertDialog({ state: DialogState.InvisibleHidden })
|
||||
expect(focusCounter).toHaveBeenCalledTimes(0)
|
||||
|
||||
// Let's open the Dialog, to see if it is not hidden anymore
|
||||
await click(document.getElementById('trigger'))
|
||||
expect(focusCounter).toHaveBeenCalledTimes(1)
|
||||
|
||||
assertDialog({ state: DialogState.Visible })
|
||||
|
||||
// Let's close the Dialog
|
||||
await press(Keys.Escape)
|
||||
expect(focusCounter).toHaveBeenCalledTimes(1)
|
||||
|
||||
assertDialog({ state: DialogState.InvisibleHidden })
|
||||
})
|
||||
|
||||
it(
|
||||
'should add a scroll lock to the html tag',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
TestRenderer, {
|
||||
allProps: [
|
||||
[ManagedDialog, { buttonText: "Trigger", buttonProps: { id: "trigger" } }, [
|
||||
[Input, { id: "a", type: "text" }],
|
||||
[Input, { id: "b", type: "text" }],
|
||||
[Input, { id: "c", type: "text" }],
|
||||
]],
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
// No overflow yet
|
||||
expect(document.documentElement.style.overflow).toBe('')
|
||||
|
||||
let btn = document.getElementById('trigger')
|
||||
|
||||
// Open the dialog
|
||||
await click(btn)
|
||||
|
||||
// Expect overflow
|
||||
expect(document.documentElement.style.overflow).toBe('hidden')
|
||||
})
|
||||
)
|
||||
})
|
||||
// TODO: more render prop tests!
|
||||
|
||||
// describe('Dialog.Overlay', () => {
|
||||
// it(
|
||||
// 'should be possible to render Dialog.Overlay using a render prop',
|
||||
// suppressConsoleLogs(async () => {
|
||||
// let overlay = jest.fn().mockReturnValue(null)
|
||||
// function Example() {
|
||||
// let [isOpen, setIsOpen] = useState(false)
|
||||
// return (
|
||||
// <>
|
||||
// <button id="trigger" onClick={() => setIsOpen(v => !v)}>
|
||||
// Trigger
|
||||
// </button>
|
||||
// <Dialog open={isOpen} onClose={setIsOpen}>
|
||||
// <Dialog.Overlay>{overlay}</Dialog.Overlay>
|
||||
// <TabSentinel />
|
||||
// </Dialog>
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
|
||||
// render(<Example />)
|
||||
|
||||
// assertDialogOverlay({
|
||||
// state: DialogState.InvisibleUnmounted,
|
||||
// attributes: { id: 'headlessui-dialog-overlay-2' },
|
||||
// })
|
||||
|
||||
// await click(document.getElementById('trigger'))
|
||||
|
||||
// assertDialogOverlay({
|
||||
// state: DialogState.Visible,
|
||||
// attributes: { id: 'headlessui-dialog-overlay-2' },
|
||||
// })
|
||||
// expect(overlay).toHaveBeenCalledWith({ open: true })
|
||||
// })
|
||||
// )
|
||||
// })
|
||||
|
||||
// describe('Dialog.Title', () => {
|
||||
// it(
|
||||
// 'should be possible to render Dialog.Title using a render prop',
|
||||
// suppressConsoleLogs(async () => {
|
||||
// render(
|
||||
// <Dialog open={true} onClose={console.log}>
|
||||
// <Dialog.Title>{JSON.stringify}</Dialog.Title>
|
||||
// <TabSentinel />
|
||||
// </Dialog>
|
||||
// )
|
||||
|
||||
// assertDialog({
|
||||
// state: DialogState.Visible,
|
||||
// attributes: { id: 'headlessui-dialog-1' },
|
||||
// })
|
||||
// assertDialogTitle({
|
||||
// state: DialogState.Visible,
|
||||
// textContent: JSON.stringify({ open: true }),
|
||||
// })
|
||||
// })
|
||||
// )
|
||||
// })
|
||||
|
||||
// describe('Dialog.Description', () => {
|
||||
// it(
|
||||
// 'should be possible to render Dialog.Description using a render prop',
|
||||
// suppressConsoleLogs(async () => {
|
||||
// render(
|
||||
// <Dialog open={true} onClose={console.log}>
|
||||
// <Dialog.Description>{JSON.stringify}</Dialog.Description>
|
||||
// <TabSentinel />
|
||||
// </Dialog>
|
||||
// )
|
||||
|
||||
// assertDialog({
|
||||
// state: DialogState.Visible,
|
||||
// attributes: { id: 'headlessui-dialog-1' },
|
||||
// })
|
||||
// assertDialogDescription({
|
||||
// state: DialogState.Visible,
|
||||
// textContent: JSON.stringify({ open: true }),
|
||||
// })
|
||||
// })
|
||||
// )
|
||||
// })
|
||||
})
|
||||
|
||||
describe('Composition', () => {
|
||||
it(
|
||||
'should be possible to open the Dialog via a Transition component',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
TestRenderer, {
|
||||
allProps: [
|
||||
Transition, { show: true }, [
|
||||
Dialog, { onClose: console.log }, [
|
||||
[DialogDescription, {}, "Description"],
|
||||
[TestTabSentinel]
|
||||
]
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
assertDialog({ state: DialogState.Visible })
|
||||
assertDialogDescription({
|
||||
state: DialogState.Visible,
|
||||
textContent: "Description",
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should be possible to close the Dialog via a Transition component',
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
TestRenderer, {
|
||||
allProps: [
|
||||
Transition, { show: false }, [
|
||||
Dialog, { onClose: console.log }, [
|
||||
[DialogDescription, {}, "Description"],
|
||||
[TestTabSentinel]
|
||||
]
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
assertDialog({ state: DialogState.InvisibleUnmounted })
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
|
||||
// This component is only for use in tests
|
||||
export let initialChecked = false;
|
||||
export let onChange = () => {};
|
||||
let state = initialChecked;
|
||||
</script>
|
||||
|
||||
<Switch checked={state} on:change={(e) => (state = e.detail)} on:change>
|
||||
<Switch
|
||||
checked={state}
|
||||
on:change={(e) => (state = e.detail)}
|
||||
on:change={onChange}
|
||||
>
|
||||
<slot />
|
||||
</Switch>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
type HandlerType = (event?: CustomEvent) => any;
|
||||
interface ComponentProps {
|
||||
onChange?: HandlerType;
|
||||
onClose?: HandlerType;
|
||||
onFocus?: HandlerType;
|
||||
}
|
||||
type SingleComponent = [SvelteComponent, ComponentProps, TestRendererProps];
|
||||
export type TestRendererProps =
|
||||
@@ -9,22 +11,30 @@
|
||||
| string
|
||||
| SingleComponent
|
||||
| SingleComponent[];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { SvelteComponent } from "svelte";
|
||||
|
||||
function isSingleComponent(
|
||||
props: SingleComponent | SingleComponent[]
|
||||
): props is SingleComponent {
|
||||
return Array.isArray(props) && !Array.isArray(props[0]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { SvelteComponent } from "svelte";
|
||||
|
||||
export let allProps: TestRendererProps;
|
||||
|
||||
let spreadProps = {};
|
||||
let onChange: HandlerType = () => {};
|
||||
let onClose: HandlerType = () => {};
|
||||
let onFocus: HandlerType = () => {};
|
||||
if (allProps && typeof allProps !== "string" && isSingleComponent(allProps)) {
|
||||
if (allProps[1].onChange) {
|
||||
onChange = allProps[1].onChange;
|
||||
}
|
||||
({
|
||||
onChange = onChange,
|
||||
onClose = onClose,
|
||||
onFocus = onFocus,
|
||||
...spreadProps
|
||||
} = allProps[1] || {});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,7 +46,13 @@
|
||||
<svelte:self allProps={childProps} />
|
||||
{/each}
|
||||
{:else}
|
||||
<svelte:component this={allProps[0]} {...allProps[1]} on:change={onChange}>
|
||||
<svelte:component
|
||||
this={allProps[0]}
|
||||
{...spreadProps}
|
||||
on:change={onChange}
|
||||
on:close={onClose}
|
||||
on:focus={onFocus}
|
||||
>
|
||||
<svelte:self allProps={allProps[2]} />
|
||||
</svelte:component>
|
||||
{/if}
|
||||
|
||||
17
src/lib/test-utils/suppress-console-logs.ts
Normal file
17
src/lib/test-utils/suppress-console-logs.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
type FunctionPropertyNames<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
|
||||
}[keyof T] &
|
||||
string
|
||||
|
||||
export function suppressConsoleLogs<T extends unknown[]>(
|
||||
cb: (...args: T) => unknown,
|
||||
type: FunctionPropertyNames<typeof global.console> = 'error'
|
||||
) {
|
||||
return (...args: T) => {
|
||||
let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn())
|
||||
|
||||
return new Promise<unknown>((resolve, reject) => {
|
||||
Promise.resolve(cb(...args)).then(resolve, reject)
|
||||
}).finally(() => spy.mockRestore())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user